Update: 2026-08-07 01:35:27
This commit is contained in:
@@ -49,6 +49,21 @@ class OrderRequestController extends GetxController
|
||||
List<dynamic>? myList;
|
||||
Map<dynamic, dynamic>? myMapData;
|
||||
|
||||
/// نوع الطلب: 'exclusive' خلال ثواني السبق الحصري، 'public' بعد إطلاقه
|
||||
/// للجميع، و null في وضع البثّ الحر (DISPATCH_MODE غير مضبوط) — عندها
|
||||
/// لا تُعرض أي شارة.
|
||||
///
|
||||
/// المصدر: حقل offer_type في حمولة السوكِت (new_ride_request /
|
||||
/// market_new_ride). الطلبات القادمة عبر FCM أو القائمة المسبقة لا تحمله.
|
||||
Map<String, dynamic>? get offerInfo {
|
||||
final t = myMapData?['offer_type'];
|
||||
if (t == null) return null;
|
||||
return {
|
||||
'offer_type': t.toString(),
|
||||
'offer_expires_in': myMapData?['offer_expires_in'],
|
||||
};
|
||||
}
|
||||
|
||||
IntaleqMapController? mapController;
|
||||
|
||||
// الإحداثيات (أزلنا late لتجنب الأخطاء القاتلة)
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/controller/home/captin/order_request_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/offer_type_badge.dart';
|
||||
import 'package:siro_driver/constant/currency.dart';
|
||||
import 'package:siro_driver/views/widgets/driver_earnings_badge.dart';
|
||||
|
||||
@@ -109,6 +110,11 @@ class OrderRequestPage extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// شارة الطلب الخاص/العام — تختفي في وضع البثّ الحر
|
||||
if (controller.offerInfo != null) ...[
|
||||
OfferTypeBadge(rideInfo: controller.offerInfo!),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
const Icon(Icons.near_me,
|
||||
color: Colors.amber, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
/// شارة نوع الطلب: خاص أم عام.
|
||||
///
|
||||
/// الخادم يمنح السائق الأفضل ترتيباً سبقاً حصرياً بضع ثوانٍ قبل أن يُطلق
|
||||
/// الطلب للجميع (DISPATCH_MODE=batched). بدون هذه الشارة يبدو الفرق
|
||||
/// عشوائياً للسائق؛ معها يفهم أنه صاحب الأولوية الآن وأن التأخير يكلّفه
|
||||
/// الطلب.
|
||||
///
|
||||
/// المصدر: حقل `offer_type` في حمولة السوكِت — 'exclusive' أو 'public'.
|
||||
/// غيابه يعني طلباً عاماً (كل المسارات القديمة والبثّ الحر)، فلا تظهر
|
||||
/// الشارة إطلاقاً في وضع البثّ العادي.
|
||||
class OfferTypeBadge extends StatelessWidget {
|
||||
final Map rideInfo;
|
||||
|
||||
const OfferTypeBadge({Key? key, required this.rideInfo}) : super(key: key);
|
||||
|
||||
bool get _isExclusive =>
|
||||
rideInfo['offer_type']?.toString() == 'exclusive';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// لا شارة في الوضع العادي — لا نضيف ضجيجاً بصرياً بلا معنى.
|
||||
if (rideInfo['offer_type'] == null) return const SizedBox.shrink();
|
||||
|
||||
final Color color = _isExclusive ? Colors.amber.shade800 : Colors.blueGrey;
|
||||
final IconData icon = _isExclusive ? Icons.star_rounded : Icons.public;
|
||||
final String label = _isExclusive ? 'طلب خاص لك'.tr : 'طلب عام'.tr;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withOpacity(0.5)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, color: color, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+88
-3
@@ -44,6 +44,7 @@ class TripOverlayService : Service() {
|
||||
private var windowManager: WindowManager? = null
|
||||
private var overlayView: View? = null
|
||||
private var countDownTimer: CountDownTimer? = null
|
||||
private var exclusivityTimer: CountDownTimer? = null
|
||||
private var currentTripId: String = ""
|
||||
private var currentFoodOrderId: String = ""
|
||||
// نوع النافذة المعروضة الآن — يحدّد لمن نُرسل نتيجة القبول/الرفض
|
||||
@@ -79,6 +80,7 @@ class TripOverlayService : Service() {
|
||||
override fun onDestroy() {
|
||||
removeOverlayView()
|
||||
countDownTimer?.cancel()
|
||||
exclusivityTimer?.cancel()
|
||||
stopSound() // 🔴 إيقاف الصوت عند التدمير
|
||||
isRunning = false
|
||||
super.onDestroy()
|
||||
@@ -176,6 +178,11 @@ class TripOverlayService : Service() {
|
||||
isRunning = true
|
||||
playSound() // 🔴 تشغيل الصوت
|
||||
startCountdown(autoCloseSeconds)
|
||||
// بعد العدّاد الرئيسي لا قبله: مؤقّتان مستقلان، والحصرية تنتهي
|
||||
// قبل النافذة بكثير فتقلب الشارة وحدها دون إغلاقها.
|
||||
if (tripData.offerType == "exclusive") {
|
||||
startExclusivityCountdown(tripData.offerExpiresIn)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
stopSelf()
|
||||
}
|
||||
@@ -430,6 +437,7 @@ class TripOverlayService : Service() {
|
||||
private fun onFoodOrderAccepted() {
|
||||
stopSound()
|
||||
countDownTimer?.cancel()
|
||||
exclusivityTimer?.cancel()
|
||||
TripOverlayPlugin.notifyFoodOrderAccepted(currentFoodOrderId)
|
||||
bringAppToForeground()
|
||||
removeOverlayView()
|
||||
@@ -553,6 +561,26 @@ class TripOverlayService : Service() {
|
||||
|
||||
detailsContainer.addView(divider(ctx, 30))
|
||||
|
||||
// ── 3.5 شارة نوع الطلب: خاص أم عام ──
|
||||
// تظهر فقط حين يرسل الخادم offerType (أي في وضع الإسناد بالدفعات).
|
||||
// في البثّ الحر تبقى مخفية تماماً فلا تضيف ضجيجاً بلا معنى.
|
||||
val offerBadge =
|
||||
TextView(ctx).apply {
|
||||
gravity = Gravity.CENTER
|
||||
textSize = 13f
|
||||
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
||||
setPadding(24, 12, 24, 12)
|
||||
visibility = if (trip.offerType.isEmpty()) View.GONE else View.VISIBLE
|
||||
applyOfferBadgeStyle(this, trip.offerType == "exclusive")
|
||||
layoutParams =
|
||||
LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
.apply { bottomMargin = 24 }
|
||||
}
|
||||
detailsContainer.addView(offerBadge)
|
||||
|
||||
// ── 4. شريط الوقت ──
|
||||
val countdownLabel =
|
||||
TextView(ctx).apply {
|
||||
@@ -621,7 +649,12 @@ class TripOverlayService : Service() {
|
||||
buttonsRow.addView(acceptBtn)
|
||||
detailsContainer.addView(buttonsRow)
|
||||
|
||||
card.tag = mapOf("countdownLabel" to countdownLabel, "progressBar" to progressBar)
|
||||
card.tag =
|
||||
mapOf(
|
||||
"countdownLabel" to countdownLabel,
|
||||
"progressBar" to progressBar,
|
||||
"offerBadge" to offerBadge
|
||||
)
|
||||
return card
|
||||
}
|
||||
// 🔴 دالة احترافية لقراءة المفتاح المحقون من Gradle 🔴
|
||||
@@ -763,6 +796,52 @@ class TripOverlayService : Service() {
|
||||
// ==========================================================
|
||||
// 🔴 التحكم والمؤقت 🔴
|
||||
// ==========================================================
|
||||
/**
|
||||
* تنسيق شارة نوع الطلب. الحصري بالكهرماني والعام بالرمادي — الفرق
|
||||
* اللوني وحده يكفي ليعرف السائق حالته بنظرة بلا قراءة.
|
||||
*/
|
||||
private fun applyOfferBadgeStyle(view: TextView, exclusive: Boolean) {
|
||||
val color = if (exclusive) "#B8860B" else "#607D8B"
|
||||
val bg = if (exclusive) "#FFF6DC" else "#ECEFF1"
|
||||
view.text = if (exclusive) "⭐ طلب خاص لك" else "🌐 طلب عام"
|
||||
view.setTextColor(Color.parseColor(color))
|
||||
view.background =
|
||||
android.graphics.drawable.GradientDrawable().apply {
|
||||
setColor(Color.parseColor(bg))
|
||||
cornerRadius = 40f
|
||||
setStroke(2, Color.parseColor(color))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* مؤقّت انتهاء الحصرية — منفصل تماماً عن مؤقّت إغلاق النافذة.
|
||||
*
|
||||
* بانقضاء ثواني السبق تصبح الرحلة معروضة للجميع، فتنقلب الشارة إلى
|
||||
* "طلب عام". النافذة **لا تُغلق**: السائق يظل قادراً على القبول، لكنه
|
||||
* صار يتنافس. الخلط بين المؤقّتين كان سيغلق النافذة بعد خمس ثوانٍ
|
||||
* ويحرمه الرحلة كلياً.
|
||||
*/
|
||||
private fun startExclusivityCountdown(seconds: Int) {
|
||||
exclusivityTimer?.cancel()
|
||||
if (seconds <= 0) return
|
||||
|
||||
exclusivityTimer =
|
||||
object : CountDownTimer(seconds * 1000L, 1000L) {
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
val left = (millisUntilFinished / 1000).toInt()
|
||||
val tagMap = overlayView?.tag as? Map<*, *> ?: return
|
||||
val badge = tagMap["offerBadge"] as? TextView ?: return
|
||||
badge.text = "⭐ طلب خاص لك — $left ث"
|
||||
}
|
||||
override fun onFinish() {
|
||||
val tagMap = overlayView?.tag as? Map<*, *> ?: return
|
||||
val badge = tagMap["offerBadge"] as? TextView ?: return
|
||||
applyOfferBadgeStyle(badge, false)
|
||||
}
|
||||
}
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun startCountdown(totalSeconds: Int) {
|
||||
countDownTimer?.cancel()
|
||||
countDownTimer =
|
||||
@@ -796,6 +875,7 @@ class TripOverlayService : Service() {
|
||||
private fun dismissOverlay(reason: String) {
|
||||
stopSound() // 🔴
|
||||
countDownTimer?.cancel()
|
||||
exclusivityTimer?.cancel()
|
||||
if (reason == "rejected" || reason == "timeout") {
|
||||
// النتيجة تذهب لقناة النوع المعروض — لا نخلط عرض توصيل بعرض رحلة
|
||||
if (isFoodOverlay) {
|
||||
@@ -843,7 +923,9 @@ class TripOverlayService : Service() {
|
||||
estimatedFare = obj.getDouble("estimatedFare"),
|
||||
estimatedMinutes = obj.getInt("estimatedMinutes"),
|
||||
pickupLat = obj.getDouble("pickupLat"),
|
||||
pickupLng = obj.getDouble("pickupLng")
|
||||
pickupLng = obj.getDouble("pickupLng"),
|
||||
offerType = obj.optString("offerType", ""),
|
||||
offerExpiresIn = obj.optInt("offerExpiresIn", 0)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
@@ -859,6 +941,9 @@ class TripOverlayService : Service() {
|
||||
val estimatedFare: Double,
|
||||
val estimatedMinutes: Int,
|
||||
val pickupLat: Double,
|
||||
val pickupLng: Double
|
||||
val pickupLng: Double,
|
||||
// "exclusive" خلال السبق الحصري، "public" بعده، وفارغ في البثّ الحر
|
||||
val offerType: String = "",
|
||||
val offerExpiresIn: Int = 0
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,14 @@ class TripData {
|
||||
final double pickupLng;
|
||||
final String? passengerAvatarUrl;
|
||||
|
||||
/// نوع الطلب: 'exclusive' خلال ثواني السبق الحصري، 'public' بعده،
|
||||
/// و null في وضع البثّ الحر (عندها لا تظهر أي شارة).
|
||||
final String? offerType;
|
||||
|
||||
/// ثواني السبق المتبقية. بعد انقضائها تقلب النافذة الشارة إلى "طلب عام"
|
||||
/// من نفسها — النافذة لا تُغلق، فالسائق يظل قادراً على القبول.
|
||||
final int offerExpiresIn;
|
||||
|
||||
TripData({
|
||||
required this.tripId,
|
||||
required this.passengerName,
|
||||
@@ -28,6 +36,8 @@ class TripData {
|
||||
required this.pickupLat,
|
||||
required this.pickupLng,
|
||||
this.passengerAvatarUrl,
|
||||
this.offerType,
|
||||
this.offerExpiresIn = 0,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() => {
|
||||
@@ -41,6 +51,8 @@ class TripData {
|
||||
'pickupLat': pickupLat,
|
||||
'pickupLng': pickupLng,
|
||||
'passengerAvatarUrl': passengerAvatarUrl ?? '',
|
||||
'offerType': offerType ?? '',
|
||||
'offerExpiresIn': offerExpiresIn,
|
||||
};
|
||||
|
||||
factory TripData.fromMap(Map<String, dynamic> map) => TripData(
|
||||
@@ -51,6 +63,10 @@ class TripData {
|
||||
distanceKm: (map['distanceKm'] ?? 0.0).toDouble(),
|
||||
estimatedFare: (map['estimatedFare'] ?? 0.0).toDouble(),
|
||||
estimatedMinutes: map['estimatedMinutes'] ?? 0,
|
||||
offerType: (map['offerType'] ?? '').toString().isEmpty
|
||||
? null
|
||||
: map['offerType'].toString(),
|
||||
offerExpiresIn: int.tryParse('${map['offerExpiresIn'] ?? 0}') ?? 0,
|
||||
pickupLat: (map['pickupLat'] ?? 0.0).toDouble(),
|
||||
pickupLng: (map['pickupLng'] ?? 0.0).toDouble(),
|
||||
passengerAvatarUrl: map['passengerAvatarUrl'],
|
||||
|
||||
Reference in New Issue
Block a user