Update: 2026-08-07 01:35:27

This commit is contained in:
Hamza-Ayed
2026-08-07 01:35:27 +03:00
parent 0cb1ab0ea3
commit 5b864e30df
11 changed files with 843 additions and 26 deletions
@@ -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,
),
),
],
),
);
}
}
@@ -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'],