Update: 2026-06-10 18:11:50
This commit is contained in:
182
siro_rider/knowledge/rider_lifecycle.md
Normal file
182
siro_rider/knowledge/rider_lifecycle.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# 🚶 دورة حياة الرحلة - تطبيق الراكب (Rider Ride Lifecycle)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
هذا الملف هو مرجع هندسي شامل ودقيق (Source of Truth) يوضح كافة تفاصيل دورة حياة الرحلة (Ride Lifecycle) في تطبيق الراكب (Siro Rider). تمت كتابته وتوثيقه ليكون دليلاً استرشادياً كاملاً لأي مهندس برمجي أو نموذج ذكاء اصطناعي (AI Agent) يرغب في فهم أو تعديل منطق تتبع السائق، استقبال أحداث الرحلة، أو معالجة الحالات المباشرة.
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 1. الكنترولر المسؤول عن الرحلة (RideLifecycleController)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
يعد
|
||||
[RideLifecycleController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart)
|
||||
هو عقل تطبيق الراكب الذي يدير الحالات والعدادات ومطابقة حركة السائق.
|
||||
</div>
|
||||
|
||||
### 1.1 التهيئة وفحص البداية (Startup & Restoration)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
* <b>فحص حالة الرحلة الأولية عند بدء التشغيل</b>:
|
||||
</div>
|
||||
|
||||
```dart
|
||||
Future<void> _checkInitialRideStatus() async
|
||||
```
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
تقوم بالخطوات التالية عند تشغيل التطبيق:
|
||||
1. استدعاء API جلب الحالة الحالية من الخادم عبر `getRideStatusFromStartApp()`.
|
||||
2. إذا وجدت رحلة معلقة أو فعالة، تقوم بمطابقة حالة الرحلة النصية وتحويلها إلى قيم الـ Enum الخاص بـ `RideState` كالتالي:
|
||||
* `"waiting"` -> `RideState.searching` (البحث عن سائق).
|
||||
* `"apply"`, `"applied"`, `"accepted"` -> `RideState.driverApplied` (السائق قبل وهو في الطريق للراكب).
|
||||
* `"arrived"` -> `RideState.driverArrived` (السائق وصل لنقطة الالتقاء).
|
||||
* `"begin"` -> `RideState.inProgress` (الرحلة بدأت بالفعل نحو الوجهة النهائية).
|
||||
* `"cancel"` -> `RideState.cancelled`.
|
||||
* `"finished"` -> إذا كانت الرحلة بحاجة لتقييم (`needsReview == 1`) تحال لـ `RideState.preCheckReview` وإلا `RideState.noRide`.
|
||||
3. استدعاء الموجه المركزي `_handleRideState()` لتوجيه واجهة المستخدم وتنشيط المهام التبعية.
|
||||
</div>
|
||||
|
||||
### 1.2 موجه ومراقب حالة الرحلة (State Machine Coordinator)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
* <b>مراقب الحالات الرئيسي</b>:
|
||||
</div>
|
||||
|
||||
```dart
|
||||
Future<void> _handleRideState(RideState state) async
|
||||
```
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
يعمل كمركز مراقبة مستمر (Polling loop fallback) يقوم بالتحقق الدوري وتحديث شاشات الراكب ومؤقتات المراقبة عند كل حالة:
|
||||
1. **`RideState.searching`**: يتحقق من فوات زمن البحث الأقصى (`_totalSearchTimeoutSeconds`). في حال انقضاء الوقت بدون موافقة، يوقف المؤشرات ويظهر نافذة زيادة السعر للراكب عبر `_showIncreaseFeeDialog()`.
|
||||
2. **`RideState.driverApplied`**: إذا كان الويب سوكيت غير متصل، يعتمد على التحديثات الدورية عبر API `getRideStatus` للتحقق مما إذا كان السائق قد وصل (`Arrived`) أو بدأ الرحلة (`Begin`).
|
||||
3. **`RideState.driverArrived`**: يقوم بتفعيل نافذة إشعار وصول السائق للراكب وبدء مؤقت الخمس دقائق المخصصة لانتظار الراكب.
|
||||
4. **`RideState.inProgress`**: في حال غياب اتصال السوكيت، يقوم بعمل استعلام دوري للتحقق مما إذا أنهى السائق الرحلة (`Finished`) لتوجيه الراكب فوراً لصفحة الدفع والتقييم.
|
||||
</div>
|
||||
|
||||
### 1.3 معالجة أحداث السائق التفاعلية (Driver Events Handler)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
* <b>قبول الطلب من السائق (Acceptance)</b>:
|
||||
</div>
|
||||
|
||||
```dart
|
||||
Future<void> processRideAcceptance({Map<String, dynamic>? driverData, required String source})
|
||||
```
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
عند قبول الطلب (سواء عبر الـ Socket أو Polling):
|
||||
1. نقل حالة التطبيق فوراً لـ `RideState.driverApplied` وتحديث الحالة لـ `'Apply'`.
|
||||
2. استخراج بيانات السائق الفردية وتخزينها محلياً عبر `_fillDriverDataLocally()`.
|
||||
3. إيقاظ خدمة الأنشطة الحية على iOS عبر `IosLiveActivityService.startRideActivity()`.
|
||||
4. إرسال إشعار فوري للراكب عبر `RideLiveNotification.showDriverOnWay()`.
|
||||
5. جلب موقع السائق الفعلي من الخريطة ورسم المسار بين السائق والراكب عبر `calculateDriverToPassengerRoute()`.
|
||||
6. تشغيل مؤقت حساب المسافة والزمن التقديري (`startTimerFromDriverToPassengerAfterApplied()`).
|
||||
</div>
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
* <b>وصول السائق للراكب (Driver Arrival)</b>:
|
||||
</div>
|
||||
|
||||
```dart
|
||||
Future<void> processDriverArrival(String source)
|
||||
```
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
1. تعيين حالة الرحلة محلياً لـ `RideState.driverArrived` وحفظ الحالة النصية لـ `'Arrived'`.
|
||||
2. عرض ديالوج وصول السائق للراكب عبر `uiInteractions.driverArrivePassengerDialoge()`.
|
||||
3. تفعيل إشعار وصول السائق العام عبر النظام.
|
||||
4. تفعيل مؤقت انتظار السائق للراكب (5 دقائق).
|
||||
5. الاستعداد المسبق ورسم الخط التقديري لمسار الرحلة الفعلي من موقع السائق إلى الوجهة النهائية للراكب.
|
||||
</div>
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
* <b>بدء الرحلة الفعلية (Begin Ride)</b>:
|
||||
</div>
|
||||
|
||||
```dart
|
||||
Future<void> processRideBegin({String source = "Unknown"})
|
||||
```
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
1. نقل حالة التطبيق لـ `RideState.inProgress` وتخزين الحالة لـ `'Begin'`.
|
||||
2. إيقاف ومسح مؤقتات الانتظار السابقة.
|
||||
3. تنظيف مسار الخريطة الأصفر (مسار بيك اب الراكب) بالكامل لمنع التداخل والتشوه البصري.
|
||||
4. رسم المسار الفعلي النهائي باللون **الأزرق** باتجاه وجهة الراكب النهائية (`myDestination`).
|
||||
5. تفعيل عداد الرحلة التفاعلي `rideIsBeginPassengerTimer()`.
|
||||
</div>
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
* <b>إنهاء الرحلة (Finish Ride)</b>:
|
||||
</div>
|
||||
|
||||
```dart
|
||||
Future<void> processRideFinished(List<dynamic> driverList, {String source = "Unknown"})
|
||||
```
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
1. نقل الحالة لـ `RideState.finished` وإيقاف وتصفير كافة عدادات ومؤقتات المراقبة.
|
||||
2. تدمير وإغلاق سوكيت الرحلة النشط عبر `mapSocket.disposeRideSocket()`.
|
||||
3. إيقاف وإغلاق خدمة الملاحة الحية على iOS والأنشطة الحية.
|
||||
4. توجيه الراكب فوراً لصفحة التقييم والدفع المخصصة `RateDriverFromPassenger` وتمرير معرف السائق والرحلة والفاتورة.
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 2. بنية تحديث الويب سوكيت للمواقع (WebSocket Location Handler)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
يتولى الكنترولر
|
||||
[MapSocketController](file:///Users/hamzaaleghwairyeen/development/App/Siro/siro_rider/lib/controller/home/map/map_socket_controller.dart)
|
||||
استقبال تحديثات السائق لحظياً عبر الحدث
|
||||
`driver_location_update`
|
||||
وتمرير البيانات المعالجة لمحركات الحساب والتحكم كالتالي:
|
||||
</div>
|
||||
|
||||
```dart
|
||||
void handleDriverLocationUpdate(dynamic data)
|
||||
```
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
الخطوات التي تنفذها الدالة:
|
||||
1. استلام وتمرير إحداثيات السائق الحالية وسرعته واتجاهه.
|
||||
2. إذا تم استلام أكثر من 3 تحديثات ناجحة ومتتالية وموثوقة عبر السوكيت، يقوم بإيقاف استعلامات الـ Polling الدورية (`stopDriverLocationPolling()`) لتوفير استهلاك البيانات والبطارية.
|
||||
3. استدعاء فحص الانحراف والمطابقة الجغرافية عبر `checkAndRecalculateIfDeviated()`.
|
||||
4. توجيه كاميرا خريطة الراكب لاتباع إحداثيات السائق مع تعديل نسبة التقريب (Zoom) ديناميكياً بناءً على سرعة السيارة (تقريب الكاميرا عند البطء وتوسيع الرؤية عند السرعة العالية).
|
||||
5. قراءة الحسابات الجاهزة للوقت المتبقي والمسافة من السيرفر إن وجدت وتمريرها، وإلا يتم استدعاء محرك الحساب المحلي للراكب عبر `updateRemainingRoute()`.
|
||||
6. تحديث موقع ماركر سيارة السائق على الخريطة بسلاسة وتمرير زاوية الدوران الصحيحة (`updateDriverMarker()`).
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 3. محرك الحساب المحلي وحماية الانحراف (Local Routing & Deviation Engine)
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
لتلافي المشاكل الجغرافية وعرض أرقام مضللة للراكب، تم بناء محركات ذكية ومحلية لمعالجة المواقع:
|
||||
</div>
|
||||
|
||||
### 3.1 مطابقة المسار وإعادة الحساب (Deviation Guard)
|
||||
|
||||
```dart
|
||||
Future<void> checkAndRecalculateIfDeviated(LatLng driverPos, {double? heading, double? speed})
|
||||
```
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
1. تقوم الدالة بمسح كافة نقاط المسار النشط ومقارنتها بموقع السائق الحالي لحساب أقرب نقطة جغرافية.
|
||||
2. إذا تجاوزت المسافة الفاصلة بين السائق والمسار المخطط قيمة حد الانحراف (`_deviationThresholdMeters` - عادة 50 متر)، أو عند تطابق انحراف الاتجاه لمرتين متتاليتين، يتم إصدار أمر فوري بإعادة جلب ورسم المسار من إحداثيات السائق الحالية:
|
||||
* إذا كانت الرحلة جارية نحو الوجهة: يتم رسم مسار جديد باتجاه وجهة الراكب النهائية.
|
||||
* إذا كان السائق في طريقه للراكب: يتم رسم مسار جديد باتجاه موقع الراكب.
|
||||
</div>
|
||||
|
||||
### 3.2 قص المسار واحتساب الـ ETA محلياً (Dynamic Route Trimming)
|
||||
|
||||
```dart
|
||||
void updateRemainingRoute(LatLng driverPos, {bool updateEta = true})
|
||||
```
|
||||
|
||||
<div dir="rtl" align="right">
|
||||
1. عند تقدم السائق، تقوم الدالة بالبحث عن أقرب نقطة على خط المسار المتجه نحو الوجهة وقص (Trim) الأجزاء التي قطعتها السيارة بالفعل (`remainingPoints = route.sublist(closestIdx)`).
|
||||
2. عند تفعيل `updateEta` (في حال عدم توفر معلومات مباشرة من السيرفر)، تقوم الدالة بحساب النسبة المئوية للمسافة المتبقية مقارنة بالمسافة الكلية الأصلية، وضربها في الزمن الكلي التقديري الأصلي لاستخراج قيمة زمن الوصول التقديري (ETA) المتبقي **محلياً وبشكل لحظي ودقيق جداً** دون الحاجة لإجراء اتصالات إضافية بالسيرفر.
|
||||
3. تحديث خطوط الـ Polylines النشطة على الخريطة بالمسار المقصوص الجديد لضمان بقائه دقيقاً.
|
||||
</div>
|
||||
@@ -87,12 +87,6 @@ class FirebaseMessagesController extends GetxController {
|
||||
fireBaseTitles(message);
|
||||
}
|
||||
});
|
||||
FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {
|
||||
// Handle background message
|
||||
if (message.data.isNotEmpty) {
|
||||
fireBaseTitles(message);
|
||||
}
|
||||
});
|
||||
|
||||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
||||
if (message.data.isNotEmpty && message.notification != null) {
|
||||
|
||||
@@ -283,7 +283,7 @@ class MapSocketController extends GetxController {
|
||||
}
|
||||
|
||||
final dynamic distanceValue =
|
||||
data['distance_m'] ?? data['distance_meters'] ?? data['distance'];
|
||||
data['distance_m'] ?? data['distance_meters'];
|
||||
final double? distanceMeters =
|
||||
double.tryParse(distanceValue?.toString() ?? '');
|
||||
final int? etaSeconds = data['eta_seconds'] == null
|
||||
|
||||
@@ -112,6 +112,7 @@ class RideLifecycleController extends GetxController {
|
||||
late String driverId = '';
|
||||
late String make = '';
|
||||
late String model = '';
|
||||
late String gender = '';
|
||||
late String carColor = '';
|
||||
late String licensePlate = '';
|
||||
late String driverName = '';
|
||||
@@ -120,6 +121,9 @@ class RideLifecycleController extends GetxController {
|
||||
late String colorHex = '';
|
||||
late String carYear = '';
|
||||
late String driverRate = '5.0';
|
||||
late String driverRatingCount = '0';
|
||||
late String driverCompletedRides = '0';
|
||||
late String driverTier = 'Verified driver';
|
||||
late String driverToken = '';
|
||||
|
||||
double kazan = 8;
|
||||
@@ -1481,7 +1485,8 @@ class RideLifecycleController extends GetxController {
|
||||
|
||||
// إيقاف جلب السيارات المجاورة ومسحها، باستثناء السائق الذي قبل الطلب
|
||||
mapEngine.reloadStartApp = false;
|
||||
mapEngine.markers.removeWhere((marker) => marker.markerId.value != driverId.toString());
|
||||
mapEngine.markers
|
||||
.removeWhere((marker) => marker.markerId.value != driverId.toString());
|
||||
mapEngine.update();
|
||||
|
||||
await getDriverCarsLocationToPassengerAfterApplied();
|
||||
@@ -1490,8 +1495,7 @@ class RideLifecycleController extends GetxController {
|
||||
LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last;
|
||||
Log.print(
|
||||
'[rideAppliedFromDriver] 📍 Driver at: $driverPos, Passenger at: $passengerLocation');
|
||||
await getInitialDriverDistanceAndDuration(driverPos, passengerLocation);
|
||||
await drawDriverPathOnly(driverPos, passengerLocation);
|
||||
await calculateDriverToPassengerRoute(driverPos, passengerLocation);
|
||||
mapEngine.fitCameraToPoints(driverPos, passengerLocation);
|
||||
}
|
||||
|
||||
@@ -1656,6 +1660,9 @@ class RideLifecycleController extends GetxController {
|
||||
driverToken = data['token']?.toString() ?? '';
|
||||
carYear = data['year']?.toString() ?? '';
|
||||
driverRate = data['ratingDriver']?.toString() ?? '5.0';
|
||||
driverRatingCount = data['ratingCount']?.toString() ?? '0';
|
||||
driverCompletedRides = data['completedRides']?.toString() ?? '0';
|
||||
driverTier = data['driverTier']?.toString() ?? 'Verified driver';
|
||||
|
||||
update();
|
||||
}
|
||||
@@ -2221,6 +2228,15 @@ class RideLifecycleController extends GetxController {
|
||||
polyLines = polyLines
|
||||
.where((p) => !p.polylineId.value.startsWith('driver_route'))
|
||||
.toSet();
|
||||
polyLines = {
|
||||
...polyLines,
|
||||
Polyline(
|
||||
polylineId: const PolylineId('main_route'),
|
||||
points: decodedPoints,
|
||||
color: const Color(0xFF2196F3),
|
||||
width: 6,
|
||||
)
|
||||
};
|
||||
} else {
|
||||
// مسح السلمات القديمة أولاً
|
||||
polyLines = polyLines
|
||||
@@ -2290,7 +2306,9 @@ class RideLifecycleController extends GetxController {
|
||||
_routeHeadingMismatchCount = 0;
|
||||
_isRecalculatingRoute = true;
|
||||
if (statusRide == 'Begin' ||
|
||||
currentRideState.value == RideState.inProgress) {
|
||||
statusRide == 'Arrived' ||
|
||||
currentRideState.value == RideState.inProgress ||
|
||||
currentRideState.value == RideState.driverArrived) {
|
||||
await calculateDriverToPassengerRoute(driverPos, myDestination,
|
||||
isBeginPhase: true);
|
||||
} else {
|
||||
@@ -2504,6 +2522,8 @@ class RideLifecycleController extends GetxController {
|
||||
String icon;
|
||||
if (model.contains('دراجة') || make.contains('دراجة')) {
|
||||
icon = mapEngine.motoIcon;
|
||||
} else if (gender == 'Female') {
|
||||
icon = mapEngine.ladyIcon;
|
||||
} else {
|
||||
icon = mapEngine.carIcon;
|
||||
}
|
||||
@@ -3026,6 +3046,17 @@ class RideLifecycleController extends GetxController {
|
||||
mapEngine.playRouteAnimation(
|
||||
mapEngine.polylineCoordinates, mapEngine.lastComputedBounds);
|
||||
}
|
||||
|
||||
if (driverCarsLocationToPassengerAfterApplied.isNotEmpty &&
|
||||
myDestination.latitude != 0 &&
|
||||
myDestination.longitude != 0) {
|
||||
await calculateDriverToPassengerRoute(
|
||||
driverCarsLocationToPassengerAfterApplied.last,
|
||||
myDestination,
|
||||
isBeginPhase: true,
|
||||
);
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -3903,12 +3934,37 @@ class RideLifecycleController extends GetxController {
|
||||
|
||||
make = data['make']?.toString() ?? '';
|
||||
model = data['model']?.toString() ?? '';
|
||||
gender = data['gender']?.toString() ?? '';
|
||||
carColor = data['color']?.toString() ?? '';
|
||||
colorHex = data['color_hex']?.toString() ?? '';
|
||||
licensePlate = data['car_plate']?.toString() ?? '';
|
||||
carYear = data['year']?.toString() ?? '';
|
||||
|
||||
// المحاولة الفورية لرسم السائق إذا توفرت الإحداثيات في البيانات
|
||||
double lat = double.tryParse(
|
||||
data['latitude']?.toString() ?? data['lat']?.toString() ?? '0') ??
|
||||
0;
|
||||
double lng = double.tryParse(data['longitude']?.toString() ??
|
||||
data['lng']?.toString() ??
|
||||
'0') ??
|
||||
0;
|
||||
double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0;
|
||||
|
||||
if (lat != 0 && lng != 0) {
|
||||
LatLng initialPos = LatLng(lat, lng);
|
||||
if (driverCarsLocationToPassengerAfterApplied.isEmpty) {
|
||||
driverCarsLocationToPassengerAfterApplied.add(initialPos);
|
||||
} else {
|
||||
driverCarsLocationToPassengerAfterApplied[0] = initialPos;
|
||||
}
|
||||
// تحديث الماركر فوراً لضمان ظهوره بشكل موثوق
|
||||
updateDriverMarker(initialPos, heading);
|
||||
}
|
||||
|
||||
driverRate = data['ratingDriver']?.toString() ?? '5.0';
|
||||
driverRatingCount = data['ratingCount']?.toString() ?? '0';
|
||||
driverCompletedRides = data['completedRides']?.toString() ?? '0';
|
||||
driverTier = data['driverTier']?.toString() ?? 'Verified driver';
|
||||
driverToken = data['token']?.toString() ?? '';
|
||||
|
||||
update();
|
||||
@@ -4185,55 +4241,6 @@ class RideLifecycleController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> getDistanceFromDriverAfterAcceptedRide(
|
||||
String origin, String destination) async {
|
||||
String apiKey = Env.mapKeyOsm;
|
||||
if (origin.isEmpty) {
|
||||
origin = '${passengerLocation.latitude},${passengerLocation.longitude}';
|
||||
}
|
||||
var uri = Uri.parse(
|
||||
'$dynamicApiUrl?origin=$origin&destination=$destination&steps=false&overview=false');
|
||||
Log.print('uri: $uri');
|
||||
|
||||
http.Response response;
|
||||
Map<String, dynamic> responseData;
|
||||
|
||||
try {
|
||||
response = await http.get(
|
||||
uri,
|
||||
headers: {
|
||||
'X-API-KEY': apiKey,
|
||||
},
|
||||
).timeout(const Duration(seconds: 20));
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
Log.print('Error from API: ${response.statusCode}');
|
||||
isLoading = false;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
if (Get.isBottomSheetOpen ?? false) {
|
||||
Get.back();
|
||||
}
|
||||
isDrawingRoute = false;
|
||||
|
||||
responseData = json.decode(response.body);
|
||||
Log.print('responseData: $responseData');
|
||||
|
||||
if (responseData['status'] != 'ok') {
|
||||
Log.print('API returned an error: ${responseData['message']}');
|
||||
isLoading = false;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('Failed to get directions: $e');
|
||||
isLoading = false;
|
||||
update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _stageNiceToHave() async {
|
||||
Log.print('🚀 MapPassengerController: Starting _stageNiceToHave');
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
@@ -15,19 +14,13 @@ import '../../../main.dart'; // contains global 'box'
|
||||
import '../../../print.dart';
|
||||
import '../../../services/emergency_signal_service.dart';
|
||||
import '../../../views/widgets/elevated_btn.dart';
|
||||
import '../../../views/widgets/mydialoug.dart';
|
||||
import '../../../views/widgets/my_textField.dart';
|
||||
import '../../../views/home/map_page_passenger.dart';
|
||||
import '../../../views/widgets/error_snakbar.dart';
|
||||
import '../../../models/model/painter_copoun.dart';
|
||||
import '../../functions/launch.dart';
|
||||
import '../../firebase/local_notification.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/tts.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import 'location_search_controller.dart';
|
||||
import 'map_engine_controller.dart';
|
||||
|
||||
class UiInteractionsController extends GetxController {
|
||||
TextEditingController sosPhonePassengerProfile = TextEditingController();
|
||||
@@ -56,54 +49,54 @@ class UiInteractionsController extends GetxController {
|
||||
|
||||
sosPhonePassengerProfile.clear();
|
||||
Get.defaultDialog(
|
||||
title: 'Add SOS Phone'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Form(
|
||||
key: sosFormKey,
|
||||
child: Column(
|
||||
children: [
|
||||
MyTextForm(
|
||||
controller: sosPhonePassengerProfile,
|
||||
label: 'insert sos phone'.tr,
|
||||
hint: 'e.g. 0912345678 (Default +963)'.tr,
|
||||
type: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Note: If no country code is entered, it will be saved as Syrian (+963).".tr,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
title: 'Add SOS Phone'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Form(
|
||||
key: sosFormKey,
|
||||
child: Column(
|
||||
children: [
|
||||
MyTextForm(
|
||||
controller: sosPhonePassengerProfile,
|
||||
label: 'insert sos phone'.tr,
|
||||
hint: 'e.g. 0912345678 (Default +963)'.tr,
|
||||
type: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Note: If no country code is entered, it will be saved as Syrian (+963)."
|
||||
.tr,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Save'.tr,
|
||||
onPressed: () async {
|
||||
if (sosFormKey.currentState!.validate()) {
|
||||
Get.back();
|
||||
var numberPhone =
|
||||
formatSyrianPhoneNumber(sosPhonePassengerProfile.text);
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Save'.tr,
|
||||
onPressed: () async {
|
||||
if (sosFormKey.currentState!.validate()) {
|
||||
Get.back();
|
||||
var numberPhone =
|
||||
formatSyrianPhoneNumber(sosPhonePassengerProfile.text);
|
||||
|
||||
await CRUD().post(
|
||||
link: AppLink.updateprofile,
|
||||
payload: {
|
||||
'id': box.read(BoxName.passengerID),
|
||||
'sosPhone': numberPhone,
|
||||
},
|
||||
);
|
||||
await CRUD().post(
|
||||
link: AppLink.updateprofile,
|
||||
payload: {
|
||||
'id': box.read(BoxName.passengerID),
|
||||
'sosPhone': numberPhone,
|
||||
},
|
||||
);
|
||||
|
||||
box.write(BoxName.sosPhonePassenger, numberPhone);
|
||||
onSuccess();
|
||||
}
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Cancel'.tr,
|
||||
onPressed: () => Get.back(),
|
||||
kolor: AppColor.redColor,
|
||||
)
|
||||
);
|
||||
box.write(BoxName.sosPhonePassenger, numberPhone);
|
||||
onSuccess();
|
||||
}
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Cancel'.tr,
|
||||
onPressed: () => Get.back(),
|
||||
kolor: AppColor.redColor,
|
||||
));
|
||||
}
|
||||
|
||||
void sosPassenger() {
|
||||
@@ -114,10 +107,12 @@ class UiInteractionsController extends GetxController {
|
||||
titleStyle: AppStyle.title.copyWith(color: AppColor.redColor),
|
||||
content: Column(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, size: 50, color: AppColor.redColor),
|
||||
Icon(Icons.warning_amber_rounded,
|
||||
size: 50, color: AppColor.redColor),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Do you want to send an emergency message to your SOS contact?".tr,
|
||||
"Do you want to send an emergency message to your SOS contact?"
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
|
||||
@@ -42,6 +42,7 @@ class MyTranslation extends Translations {
|
||||
"Arrived": "وصلنا",
|
||||
"Audio Recording": "تسجيل صوتي",
|
||||
"Call": "اتصال",
|
||||
"Call Options": "خيارات الاتصال",
|
||||
"Call Connected": "تم فتح الاتصال",
|
||||
"Call Support": "اتصل بالدعم",
|
||||
"Call left": "مكالمات متبقية",
|
||||
@@ -49,6 +50,8 @@ class MyTranslation extends Translations {
|
||||
"Change Photo": "تغيير الصورة",
|
||||
"Captain": "الكابتن",
|
||||
"Choose from Gallery": "اختر من المعرض",
|
||||
"Choose how you want to call the driver":
|
||||
"اختر طريقة الاتصال بالكابتن",
|
||||
"Choose from contact": "اختر من جهات الاتصال",
|
||||
"Click to track the trip": "اضغط لتتبع المشوار",
|
||||
"Close panel": "إغلاق اللوحة",
|
||||
@@ -92,6 +95,9 @@ class MyTranslation extends Translations {
|
||||
"Finished": "انتهى",
|
||||
"Fixed Price": "سعر ثابت",
|
||||
"Free Call": "مكالمة مجانية",
|
||||
"Professional driver": "كابتن محترف",
|
||||
"Trusted driver": "كابتن موثوق",
|
||||
"Verified driver": "كابتن موثق",
|
||||
"General": "عام",
|
||||
"Grant": "منح الإذن",
|
||||
"Have a Promo Code?": "معك كود خصم؟",
|
||||
@@ -178,6 +184,7 @@ class MyTranslation extends Translations {
|
||||
"Preferences": "التفضيلات",
|
||||
"Profile photo updated": "تم تحديث صورة الغلاف",
|
||||
"Quick Message": "رسالة سريعة",
|
||||
"reviews": "تقييم",
|
||||
"Rating is": "التقييم هو",
|
||||
"Received empty route data.": "تم استلام بيانات طريق فارغة.",
|
||||
"Record": "تسجيل",
|
||||
@@ -211,6 +218,7 @@ class MyTranslation extends Translations {
|
||||
"Set as Work": "تحديد كالشغل",
|
||||
"Share": "مشاركة",
|
||||
"Share Trip": "مشاركة المشوار",
|
||||
"Standard Call": "اتصال عادي",
|
||||
"Share your experience to help us improve...":
|
||||
"شاركنا تجربتك لنحسن خدمتنا...",
|
||||
"Something went wrong. Please try again.": "صار غلط. جرب مرة تانية.",
|
||||
@@ -271,6 +279,8 @@ class MyTranslation extends Translations {
|
||||
"to arrive you.": "ليوصلك.",
|
||||
"unknown": "غير معروف",
|
||||
"wait 1 minute to recive message": "استنى دقيقة لتستلم الرسالة",
|
||||
"Uses cellular network": "يستخدم شبكة الهاتف",
|
||||
"Voice call over internet": "مكالمة صوتية عبر الإنترنت",
|
||||
"with license plate": "برقم اللوحة",
|
||||
"witout zero": "بدون صفر",
|
||||
"you must insert token code": "لازم تدخل الكود",
|
||||
@@ -16885,7 +16895,8 @@ class MyTranslation extends Translations {
|
||||
"Support is Away": "سپورٹ اب دستیاب نہیں ہے",
|
||||
"Support is currently Online": "سپورٹ اب آن لائن ہے",
|
||||
"Voice Call": "صوتی کال",
|
||||
"We're here to help you 24/7": "ہم چوبیس گھنٹے آپ کی مدد کے لیے حاضر ہیں",
|
||||
"We're here to help you 24/7":
|
||||
"ہم چوبیس گھنٹے آپ کی مدد کے لیے حاضر ہیں",
|
||||
"Working Hours:": "کام کے اوقات:",
|
||||
"1 Passenger": "1 Passenger",
|
||||
"2 Passengers": "2 Passengers",
|
||||
@@ -18446,7 +18457,8 @@ class MyTranslation extends Translations {
|
||||
"Support is Away": "सहायता अभी उपलब्ध नहीं है",
|
||||
"Support is currently Online": "सहायता अभी ऑनलाइन है",
|
||||
"Voice Call": "वॉइस कॉल",
|
||||
"We're here to help you 24/7": "हम आपकी सहायता के लिए 24/7 उपलब्ध हैं",
|
||||
"We're here to help you 24/7":
|
||||
"हम आपकी सहायता के लिए 24/7 उपलब्ध हैं",
|
||||
"Working Hours:": "कार्य समय:",
|
||||
"1 Passenger": "1 Passenger",
|
||||
"2 Passengers": "2 Passengers",
|
||||
|
||||
@@ -250,19 +250,23 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
// صورة السائق (أصغر)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColor.primaryColor.withOpacity(0.2), width: 2),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 22, // تصغير من 28 إلى 22
|
||||
backgroundColor: Colors.grey[200],
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'),
|
||||
onBackgroundImageError: (_, __) =>
|
||||
const Icon(Icons.person, color: Colors.grey, size: 20),
|
||||
GestureDetector(
|
||||
onTap: () => _showDriverAvatarDialog(context, controller),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColor.primaryColor.withOpacity(0.2),
|
||||
width: 2),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 22, // تصغير من 28 إلى 22
|
||||
backgroundColor: Colors.grey[200],
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'),
|
||||
onBackgroundImageError: (_, __) =>
|
||||
const Icon(Icons.person, color: Colors.grey, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -299,6 +303,32 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
_buildDriverBadge(
|
||||
icon: Icons.verified_rounded,
|
||||
text: controller.driverTier.tr,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
if (controller.driverCompletedRides != '0')
|
||||
_buildDriverBadge(
|
||||
icon: Icons.route_rounded,
|
||||
text:
|
||||
'${controller.driverCompletedRides} ${'rides'.tr}',
|
||||
color: Colors.teal,
|
||||
),
|
||||
if (controller.driverRatingCount != '0')
|
||||
_buildDriverBadge(
|
||||
icon: Icons.reviews_rounded,
|
||||
text:
|
||||
'${controller.driverRatingCount} ${'reviews'.tr}',
|
||||
color: Colors.amber.shade800,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -320,6 +350,11 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
Widget _buildMicroCarIcon(
|
||||
RideLifecycleController controller, Color Function(String) parseColor) {
|
||||
Color carColor = parseColor(controller.colorHex);
|
||||
final String vehicleText =
|
||||
'${controller.model} ${controller.make}'.toLowerCase();
|
||||
final bool isBike = vehicleText.contains('scooter') ||
|
||||
vehicleText.contains('bike') ||
|
||||
vehicleText.contains('دراجة');
|
||||
return Container(
|
||||
height: 40, // تصغير من 50
|
||||
width: 40,
|
||||
@@ -331,7 +366,8 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(carColor, BlendMode.srcIn),
|
||||
child: Image.asset(
|
||||
box.read(BoxName.carType) == 'Scooter' ||
|
||||
isBike ||
|
||||
box.read(BoxName.carType) == 'Scooter' ||
|
||||
box.read(BoxName.carType) == 'Pink Bike'
|
||||
? 'assets/images/moto.png'
|
||||
: 'assets/images/car3.png',
|
||||
@@ -341,6 +377,81 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDriverBadge({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
required Color color,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 11, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDriverAvatarDialog(
|
||||
BuildContext context, RideLifecycleController controller) {
|
||||
final imageUrl =
|
||||
'${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg';
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 38),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(18, 20, 18, 18),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 58,
|
||||
backgroundColor: Colors.grey[200],
|
||||
backgroundImage: NetworkImage(imageUrl),
|
||||
onBackgroundImageError: (_, __) {},
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
controller.driverName,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'${controller.driverTier.tr} • ${controller.driverRate}',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
barrierDismissible: true,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSlimLicensePlate(String plateNumber) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
|
||||
Reference in New Issue
Block a user