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
@@ -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'],