26-1-21/1
This commit is contained in:
@@ -47,8 +47,8 @@ android {
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = 23
|
||||
targetSdk = 36
|
||||
versionCode = 45
|
||||
versionName = '1.0.45'
|
||||
versionCode = 57
|
||||
versionName = '1.0.57'
|
||||
multiDexEnabled = true
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "arm64-v8a"
|
||||
|
||||
BIN
assets/images/shamcashsend.png
Normal file
BIN
assets/images/shamcashsend.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
24
collect_code.py
Normal file
24
collect_code.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import os
|
||||
|
||||
# اسم الملف الناتج
|
||||
output_filename = "full_project_code.txt"
|
||||
# المجلد الذي تريد سحب الكود منه
|
||||
source_folder = "./lib"
|
||||
|
||||
with open(output_filename, "w", encoding="utf-8") as outfile:
|
||||
for root, dirs, files in os.walk(source_folder):
|
||||
for file in files:
|
||||
if file.endswith(".dart"):
|
||||
file_path = os.path.join(root, file)
|
||||
# كتابة فاصل واسم الملف ليعرف الذكاء الاصطناعي أين يبدأ الملف
|
||||
outfile.write(f"\n\n{'='*50}\n")
|
||||
outfile.write(f"FILE PATH: {file_path}\n")
|
||||
outfile.write(f"{'='*50}\n\n")
|
||||
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as infile:
|
||||
outfile.write(infile.read())
|
||||
except Exception as e:
|
||||
outfile.write(f"Error reading file: {e}\n")
|
||||
|
||||
print(f"تم تجميع الكود بنجاح في الملف: {output_filename}")
|
||||
85287
full_project_code.txt
Normal file
85287
full_project_code.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -33,11 +33,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>23</string>
|
||||
<string>30</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0.23</string>
|
||||
<string>1.1.30</string>
|
||||
<key>FirebaseAppDelegateProxyEnabled</key>
|
||||
<string>NO</string>
|
||||
<key>GMSApiKey</key>
|
||||
|
||||
194
lib/README.md
Normal file
194
lib/README.md
Normal file
@@ -0,0 +1,194 @@
|
||||
إليك التقرير الفني الشامل لمشروع **Intaleq Passenger App**، مكتوباً بصيغة توثيق تقني (Technical Documentation) موجهة لفريق التطوير، بناءً على تحليل الكود المصدري.
|
||||
|
||||
---
|
||||
|
||||
# 📘 Intaleq Passenger App - Technical Documentation Report
|
||||
|
||||
**Prepared by:** CTO Office
|
||||
**Target Audience:** Mobile Engineering Team
|
||||
**Version:** 1.0
|
||||
|
||||
---
|
||||
|
||||
## 1. 🚀 بداية التشغيل (Initialization & Startup)
|
||||
|
||||
تعتمد مرحلة الإقلاع على تهيئة الخدمات الأساسية قبل عرض واجهة المستخدم لضمان استقرار التطبيق.
|
||||
|
||||
### أ. نقطة الدخول (`main.dart`)
|
||||
|
||||
- **المسار:** `lib/main.dart`
|
||||
- **الوظيفة:**
|
||||
- يستخدم `runZonedGuarded` لالتقاط الأخطاء العامة (Global Error Handling) وإرسالها للسيرفر عبر `CRUD.addError`.
|
||||
- **تهيئة الخدمات:** يتم تهيئة `GetStorage` (للتخزين المحلي)، `WakelockPlus` (لمنع انطفاء الشاشة)، و `Firebase` (للإشعارات) قبل استدعاء `runApp`.
|
||||
- **إعدادات التوجيه:** يتم تحديد الاتجاه العمودي فقط (`portraitUp`) للجهاز.
|
||||
- **حقن التبعيات (DI):** يتم استدعاء `AppBindings` كـ `initialBinding` لتهيئة المتحكمات الأساسية.
|
||||
|
||||
### ب. إدارة التبعيات (`AppBindings`)
|
||||
|
||||
- **المسار:** `lib/app_bindings.dart`
|
||||
- **الآلية:**
|
||||
- يتم حقن `LocaleController` و `DeepLinkController` بشكل دائم (`permanent: true`) لضمان تواجدهم طوال دورة حياة التطبيق.
|
||||
- يتم استخدام `Get.lazyPut` مع `fenix: true` للمتحكمات الأخرى (مثل `LoginController`) ليتم إنشاؤها عند الحاجة وإعادة إنشائها إذا تم التخلص منها.
|
||||
|
||||
### ج. شاشة البداية (`Splash Screen`)
|
||||
|
||||
- **المسار:** `lib/splash_screen_page.dart` و `lib/controller/home/splash_screen_controlle.dart`
|
||||
- **المنطق:**
|
||||
- يتم عرض انيميشن باستخدام `AnimatedTextKit` و `FadeTransition`.
|
||||
- **العمليات الخلفية:** يقوم `SplashScreenController` بتنفيذ `_initializeBackgroundServices` لتهيئة خدمات التشفير (`EncryptionHelper`) والإشعارات.
|
||||
- **التوجيه الذكي:** تتحقق الدالة `_performNavigationLogic` من وجود بيانات المستخدم في `BoxName`. إذا كان المستخدم مسجلاً ومفعلاً (`isVerified == '1'`)، يتم توجيهه تلقائياً للصفحة الرئيسية، وإلا يتم توجيهه لصفحة الدخول أو الترحيب (`OnBoarding`).
|
||||
|
||||
---
|
||||
|
||||
## 2. 🔐 دورة المصادقة (Authentication Cycle)
|
||||
|
||||
يعتمد النظام على مصادقة هجينة (Token-based + Social Auth) مع تخزين آمن.
|
||||
|
||||
### أ. التسجيل والدخول (`Sign Up & Login`)
|
||||
|
||||
- **المسار:** `lib/controller/auth/login_controller.dart` و `register_controller.dart`
|
||||
- **الآلية:**
|
||||
- **Social Login:** يتم استخدام `GoogleSignInHelper` أو `AuthController` (Apple). عند النجاح، يتم إرسال التوكن للسيرفر للتحقق.
|
||||
- **Credentials:** في حالة الدخول التقليدي، يتم استدعاء `loginUsingCredentials` التي تتحقق من البيانات عبر API.
|
||||
- **التحقق (Verification):** إذا رد السيرفر بأن الحساب غير مفعل، يتم تحويل المستخدم لصفحة `PhoneNumberScreen` للتحقق عبر OTP.
|
||||
|
||||
### ب. التحقق عبر الهاتف (OTP)
|
||||
|
||||
- **المسار:** `lib/controller/auth/otp_controller.dart`
|
||||
- **المنطق:** يستخدم كلاس `PhoneAuthHelper` لإرسال OTP (غالباً عبر WhatsApp أو SMS حسب الدولة) ثم التحقق منه عبر الـ Endpoint `verifyOtp.php`.
|
||||
|
||||
### ج. إدارة الجلسة والتوكن
|
||||
|
||||
- **المخزن:** يتم تخزين الـ JWT في `GetStorage` تحت مفتاح `BoxName.jwt`.
|
||||
- **التشفير:** يتم استخدام `EncryptionHelper` لتشفير البيانات الحساسة محلياً.
|
||||
- **التجديد التلقائي:** في كلاس `CRUD`، إذا رد السيرفر بـ `401 Token expired`، يتم استدعاء `getJWT` تلقائياً لتجديد التوكن دون تسجيل خروج المستخدم.
|
||||
|
||||
---
|
||||
|
||||
## 3. 🗺️ الشاشة الرئيسية والخريطة (Home & Map Logic)
|
||||
|
||||
تعتبر `MapPagePassenger` هي الواجهة المركزية التي تديرها `MapPassengerController`.
|
||||
|
||||
### أ. تحميل الخريطة والموقع
|
||||
|
||||
- **المسار:** `lib/controller/home/map_passenger_controller.dart`
|
||||
- **المتحكم:** `MapPassengerController`
|
||||
- **المنطق:**
|
||||
- يتم استخدام `location.getLocation()` لجلب موقع الراكب الحالي عند البدء.
|
||||
- يتم تحديد المنطقة الجغرافية (سوريا، مصر، الأردن) عبر دالة `getLocationArea` التي تفحص وقوع الإحداثيات داخل مضلعات (Polygons) محددة مسبقاً.
|
||||
|
||||
### ب. السيارات القريبة (Real-time Updates)
|
||||
|
||||
- **الدالة:** `getCarsLocationByPassengerAndReloadMarker`.
|
||||
- **الآلية:** تقوم بطلب API (مثل `getSpeed.php`) بناءً على نوع السيارة المختار (Speed, Comfort, Lady). يتم تحديث الـ `markers` على الخريطة، ويتم استخدام دالة `_smoothlyUpdateMarker` لتحريك أيقونة السيارة بسلاسة بدلاً من القفز المفاجئ.
|
||||
|
||||
### ج. القائمة الجانبية (Drawer)
|
||||
|
||||
- **المسار:** `lib/views/home/map_widget.dart/map_menu_widget.dart`
|
||||
- **المتحكم:** `MyMenuController`
|
||||
- **الوظيفة:** تدير حالة القائمة (مفتوحة/مغلقة) وتوفر روابط لصفحات الملف الشخصي، المحفظة، والسجل.
|
||||
|
||||
---
|
||||
|
||||
## 4. 🚕 دورة طلب الرحلة (Ride Request Flow)
|
||||
|
||||
هذا هو الجزء الأكثر تعقيداً في التطبيق، حيث يدار عبر آلة حالة (State Machine).
|
||||
|
||||
### أ. اختيار الوجهة ورسم المسار
|
||||
|
||||
- **المسار:** `lib/controller/home/map_passenger_controller.dart`
|
||||
- **الوظيفة:** `getDirectionMap`.
|
||||
- **المنطق:**
|
||||
- يتم إرسال نقطة البداية والنهاية إلى خدمة التوجيه (OSRM/Google).
|
||||
- يتم استلام نقاط المسار (Polyline Points) وفك تشفيرها في `Isolate` منفصل (`decodePolylineIsolate`) لتحسين الأداء.
|
||||
- يتم رسم المسار على الخريطة وضبط الكاميرا لتشمل النقطتين.
|
||||
|
||||
### ب. اختيار نوع السيارة والسعر
|
||||
|
||||
- **المسار:** `lib/views/home/map_widget.dart/car_details_widget_to_go.dart`
|
||||
- **الآلية:** يتم عرض قائمة أنواع السيارات (Fixed Price, Comfort, etc.). عند الاختيار، يتم حساب السعر المتوقع بناءً على المسافة والوقت والتعرفة الخاصة بكل نوع والمخزنة في المتحكم (`totalPassengerSpeed`, `totalPassengerComfort`).
|
||||
|
||||
### ج. إرسال الطلب (البحث عن سائق)
|
||||
|
||||
- **الدالة:** `startSearchingForDriver`.
|
||||
- **العمليات:**
|
||||
1. تغيير الحالة إلى `RideState.searching`.
|
||||
2. استدعاء `postRideDetailsToServer` لإنشاء سجل الرحلة في قاعدة البيانات.
|
||||
3. تشغيل المؤقت `_startMasterTimer` الذي يدير دورة البحث.
|
||||
4. يتم توسيع نطاق البحث (Radius) تدريجياً (مراحل: 2400م -> 3000م -> 3100م) عبر `_findAndNotifyNearestDrivers`.
|
||||
|
||||
### د. انتظار السائق
|
||||
|
||||
- **الواجهة:** `SearchingCaptainWindow`.
|
||||
- **المنطق:** يظهر رادار بحث. يتم التحقق دورياً (`Polling`) من حالة الرحلة في السيرفر. إذا مر الوقت المحدد (90 ثانية) دون قبول، يظهر خيار "زيادة السعر" (`_showIncreaseFeeDialog`).
|
||||
|
||||
---
|
||||
|
||||
## 5. 🚘 أثناء الرحلة (Active Ride)
|
||||
|
||||
يتم إدارة هذه المرحلة عبر تحديثات الحالة في `_handleRideState`.
|
||||
|
||||
### أ. قبول السائق
|
||||
|
||||
- **الحدث:** وصول إشعار FCM أو تغيير الحالة في السيرفر إلى `Apply`.
|
||||
- **الإجراء:** يتم استدعاء `processRideAcceptance`.
|
||||
- يتم جلب بيانات السائق وموقعه.
|
||||
- يتم تفعيل تتبع السائق `startTimerFromDriverToPassengerAfterApplied`.
|
||||
- تتغير الواجهة لعرض معلومات السائق والوقت المقدر للوصول.
|
||||
|
||||
### ب. بدء الرحلة وميزات الأمان
|
||||
|
||||
- **بدء الرحلة:** عند وصول حالة `Begin`، يتم استدعاء `processRideBegin`.
|
||||
- **SOS:** زر الاستغاثة يستدعي `makePhoneCall` مع رقم الشرطة أو جهة اتصال الطوارئ المخزنة.
|
||||
- **مشاركة الرحلة:** الدالة `shareTripWithFamily` تولد رابط تتبع مشفر وترسله عبر واتساب.
|
||||
- **تسجيل الصوت:** يتم استخدام `AudioRecorderController` لتسجيل ما يدور في الرحلة لأغراض الأمان.
|
||||
|
||||
### ج. إلغاء الرحلة
|
||||
|
||||
- **الدالة:** `cancelRide`.
|
||||
- **المنطق:**
|
||||
- يتم إرسال طلب `cancel` للسيرفر لتحديث حالة الرحلة.
|
||||
- يتم إرسال إشعار للسائق بالإلغاء.
|
||||
- يتم تصفير الواجهة والعودة للخريطة.
|
||||
|
||||
---
|
||||
|
||||
## 6. 🏁 ما بعد الرحلة (Post-Ride)
|
||||
|
||||
### أ. شاشة الدفع
|
||||
|
||||
- **المسار:** `lib/controller/payment/payment_controller.dart`
|
||||
- **المنطق:**
|
||||
- يتم الخصم من المحفظة (`addPassengerWallet`) أو الدفع النقدي.
|
||||
- يتم التعامل مع بوابات الدفع الخارجية (مثل Paymob, Stripe) إذا اختار العميل الدفع الإلكتروني.
|
||||
|
||||
### ب. التقييم
|
||||
|
||||
- **المسار:** `lib/views/Rate/rate_captain.dart` و `RateController`.
|
||||
- **الآلية:** يقوم الراكب باختيار عدد النجوم وإضافة تعليق. يتم إرسال البيانات عبر `addRateToDriver`.
|
||||
|
||||
### ج. تقديم الشكاوى
|
||||
|
||||
- **المسار:** `lib/views/home/profile/complaint_page.dart`
|
||||
- **المتحكم:** `ComplaintController`.
|
||||
- **الميزة:** يمكن للراكب تسجيل رسالة صوتية وإرفاقها مع الشكوى، ثم يتم إرسالها للسيرفر عبر `submitComplaintToServer`.
|
||||
|
||||
---
|
||||
|
||||
## 7. ⚙️ الإعدادات والملف الشخصي
|
||||
|
||||
### أ. تعديل البيانات
|
||||
|
||||
- **المسار:** `lib/views/home/profile/passenger_profile_page.dart`.
|
||||
- **المتحكم:** `ProfileController`.
|
||||
- يسمح بتعديل الاسم، الجنس، ورقم الطوارئ.
|
||||
|
||||
### ب. المحفظة
|
||||
|
||||
- **المسار:** `lib/views/home/my_wallet/passenger_wallet.dart`.
|
||||
- يعرض الرصيد الحالي وسجل المعاملات (`PassengerWalletHistoryController`).
|
||||
|
||||
### ج. اللغة
|
||||
|
||||
- **المتحكم:** `LocaleController`.
|
||||
- يقوم بتغيير لغة التطبيق وتحديث `Get.updateLocale` وحفظ التفضيل في التخزين المحلي.
|
||||
@@ -6,13 +6,15 @@ class AppLink {
|
||||
|
||||
static String paymentServer = 'https://walletintaleq.intaleq.xyz/v1/main';
|
||||
static String location = 'https://api.intaleq.xyz/intaleq/ride/location';
|
||||
|
||||
static String locationServer =
|
||||
'https://location.intaleq.xyz/intaleq/ride/location';
|
||||
static String seferPaymentServer0 = box.read('seferPaymentServer');
|
||||
|
||||
static final String endPoint = 'https://api.intaleq.xyz/intaleq';
|
||||
static final String ride = 'https://rides.intaleq.xyz/intaleq';
|
||||
// box.read(BoxName.serverChosen) ?? box.read(BoxName.basicLink);
|
||||
static final String server = 'https://api.intaleq.xyz/intaleq';
|
||||
static final String serverSocket = 'https://rides.intaleq.xyz';
|
||||
static String IntaleqSyriaServer = endPoint;
|
||||
static String IntaleqGizaServer = box.read('Giza');
|
||||
static String IntaleqAlexandriaServer = box.read('Alexandria');
|
||||
@@ -244,7 +246,8 @@ class AppLink {
|
||||
|
||||
static String getLocationAreaLinks =
|
||||
'$server/ride/location/get_location_area_links.php';
|
||||
static String addpassengerLocation = "$location/addpassengerLocation.php";
|
||||
static String addpassengerLocation =
|
||||
"$locationServer/addpassengerLocation.php";
|
||||
static String getCarsLocationByPassengerSpeed = "$location/getSpeed.php";
|
||||
static String getCarsLocationByPassengerComfort = "$location/getComfort.php";
|
||||
static String getCarsLocationByPassengerBalash = "$location/getBalash.php";
|
||||
|
||||
@@ -105,6 +105,9 @@ class FirebaseMessagesController extends GetxController {
|
||||
// اقرأ "النوع" من حمولة البيانات، وليس من العنوان
|
||||
String category = message.data['category'] ?? '';
|
||||
|
||||
final mapCtrl = Get.isRegistered<MapPassengerController>()
|
||||
? Get.find<MapPassengerController>()
|
||||
: null;
|
||||
// اقرأ العنوان (للعرض)
|
||||
String title = message.notification?.title ?? '';
|
||||
String body = message.notification?.body ?? '';
|
||||
@@ -119,17 +122,25 @@ class FirebaseMessagesController extends GetxController {
|
||||
|
||||
// ... داخل معالج الإشعارات في تطبيق الراكب ...
|
||||
else if (category == 'Accepted Ride') {
|
||||
// <-- كان 'Accepted Ride'
|
||||
var driverListJson = message.data['driverList'];
|
||||
if (driverListJson != null) {
|
||||
var myList = jsonDecode(driverListJson) as List<dynamic>;
|
||||
final controller = Get.find<MapPassengerController>();
|
||||
// controller.currentRideState.value = RideState.driverApplied;
|
||||
await controller.processRideAcceptance(
|
||||
driverIdFromFCM: myList[0].toString(),
|
||||
rideIdFromFCM: myList[3].toString());
|
||||
} else {
|
||||
Log.print('❌ خطأ: RIDE_ACCEPTED وصل بدون driverList');
|
||||
if (mapCtrl != null) {
|
||||
Map<String, dynamic>? driverInfoMap;
|
||||
|
||||
// 2. معالجة driver_info (تأتي كـ String JSON من PHP)
|
||||
if (message.data['driver_info'] != null) {
|
||||
try {
|
||||
String rawJson = message.data['driver_info'];
|
||||
// 🔥 فك التشفير: تحويل الـ String إلى Map
|
||||
driverInfoMap = jsonDecode(rawJson);
|
||||
} catch (e) {
|
||||
print("❌ Error decoding FCM driver_info: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. تمرير البيانات الجاهزة للكنترولر
|
||||
await mapCtrl.processRideAcceptance(
|
||||
driverData: driverInfoMap,
|
||||
source: "FCM",
|
||||
);
|
||||
}
|
||||
} else if (category == 'Promo') {
|
||||
// <-- كان 'Promo'.tr
|
||||
@@ -142,7 +153,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'iphone_ringtone');
|
||||
}
|
||||
var myListString = message.data['DriverList'];
|
||||
var myListString = message.data['passengerList'];
|
||||
var myList = jsonDecode(myListString) as List<dynamic>;
|
||||
Get.to(() => TripMonitor(), arguments: {
|
||||
'rideId': myList[0].toString(),
|
||||
@@ -161,7 +172,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'tone1');
|
||||
}
|
||||
} else if (category == 'message From passenger') {
|
||||
} else if (category == 'MSG_FROM_PASSENGER') {
|
||||
// <-- كان 'message From passenger'
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'ding');
|
||||
@@ -178,78 +189,33 @@ class FirebaseMessagesController extends GetxController {
|
||||
} else if (category == 'Trip is Begin') {
|
||||
// <-- كان 'Trip is Begin'
|
||||
Log.print('[FCM] استقبل إشعار "TRIP_BEGUN".');
|
||||
final controller = Get.find<MapPassengerController>();
|
||||
controller.processRideBegin();
|
||||
// استدعاء الحارس
|
||||
mapCtrl!.processRideBegin(source: "FCM");
|
||||
} else if (category == 'Hi ,I will go now') {
|
||||
// <-- كان 'Hi ,I will go now'.tr
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'ding');
|
||||
}
|
||||
update();
|
||||
} else if (category == 'Hi ,I Arrive your site') {
|
||||
// <-- كان 'Hi ,I Arrive your site'.tr
|
||||
final controller = Get.find<MapPassengerController>();
|
||||
// if (controller.currentRideState.value == RideState.driverApplied) {
|
||||
Log.print('[FCM] السائق وصل. تغيير الحالة إلى driverArrived');
|
||||
controller.currentRideState.value = RideState.driverArrived;
|
||||
// }
|
||||
} else if (category == 'Cancel Trip from driver') {
|
||||
// <-- كان "Cancel Trip from driver"
|
||||
Get.back();
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'cancel');
|
||||
}
|
||||
Get.defaultDialog(
|
||||
title: "The driver canceled your ride.".tr, // العنوان المترجم للعرض
|
||||
middleText: "We will look for a new driver.\nPlease wait.".tr,
|
||||
confirm: MyElevatedButton(
|
||||
kolor: AppColor.greenColor,
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
await Get.find<MapPassengerController>()
|
||||
.reSearchAfterCanceledFromDriver();
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Cancel'.tr,
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () {
|
||||
Get.offAll(() => const MapPagePassenger());
|
||||
},
|
||||
));
|
||||
} else if (category == "Arrive Ride") {
|
||||
// استدعاء الحارس
|
||||
mapCtrl!.processDriverArrival("FCM");
|
||||
} else if (category == 'Driver Finish Trip') {
|
||||
// <-- كان 'Driver Finish Trip'.tr
|
||||
final rawData = message.data['DriverList'];
|
||||
List<dynamic> driverList = [];
|
||||
if (rawData != null && rawData is String) {
|
||||
|
||||
// ✅ معالجة آمنة للبيانات
|
||||
var rawData = message.data['DriverList'];
|
||||
if (rawData != null && rawData.isNotEmpty) {
|
||||
try {
|
||||
driverList = jsonDecode(rawData);
|
||||
driverList = jsonDecode(rawData) as List<dynamic>;
|
||||
} catch (e) {
|
||||
Log.print('Error decoding DriverList JSON: $e');
|
||||
print("❌ Error decoding DriverList: $e");
|
||||
}
|
||||
} else {
|
||||
Log.print('Error: DriverList data is null or not a String.');
|
||||
}
|
||||
|
||||
if (driverList.length >= 3) {
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(
|
||||
title,
|
||||
'${'you will pay to Driver'.tr} ${driverList[3].toString()} \$',
|
||||
'tone1');
|
||||
}
|
||||
Get.find<AudioRecorderController>().stopRecording();
|
||||
// ... (باقي كود المحفظة) ...
|
||||
Get.find<MapPassengerController>().tripFinishedFromDriver();
|
||||
// ... (إشعار "لا تنسى متعلقاتك") ...
|
||||
Get.to(() => RateDriverFromPassenger(), arguments: {
|
||||
'driverId': driverList[0].toString(),
|
||||
'rideId': driverList[1].toString(),
|
||||
'price': driverList[3].toString()
|
||||
});
|
||||
} else {
|
||||
Log.print('Error: TRIP_FINISHED decoded list error.');
|
||||
if (driverList.isNotEmpty) {
|
||||
Get.find<MapPassengerController>()
|
||||
.processRideFinished(driverList, source: "FCM");
|
||||
}
|
||||
} else if (category == 'Finish Monitor') {
|
||||
// <-- كان "Finish Monitor".tr
|
||||
@@ -262,19 +228,21 @@ class FirebaseMessagesController extends GetxController {
|
||||
onPressed: () {
|
||||
Get.offAll(() => const MapPagePassenger());
|
||||
}));
|
||||
} else if (category == 'Driver Cancelled Your Trip') {
|
||||
} else if (category == 'Cancel Trip from driver') {
|
||||
Log.print("🔔 FCM: Ride Cancelled by Driver received.");
|
||||
|
||||
// لا داعي لكتابة منطق التنظيف هنا، الكنترولر يتكفل بكل شيء
|
||||
if (Get.isRegistered<MapPassengerController>()) {
|
||||
// استدعاء الحارس (سيتجاهل الأمر إذا كان السوكيت قد سبقه)
|
||||
Get.find<MapPassengerController>()
|
||||
.processRideCancelledByDriver(message.data, source: "FCM");
|
||||
}
|
||||
|
||||
// إشعار محلي (اختياري، لأن الديالوج سيظهر)
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(
|
||||
'Driver Cancelled Your Trip'.tr,
|
||||
'you will pay to Driver you will be pay the cost of driver time look to your Intaleq Wallet'
|
||||
.tr,
|
||||
'cancel');
|
||||
'Trip Cancelled'.tr, 'The driver cancelled the trip.'.tr, 'cancel');
|
||||
}
|
||||
box.write(BoxName.parentTripSelected, false);
|
||||
box.remove(BoxName.tokenParent);
|
||||
|
||||
Get.find<MapPassengerController>().restCounter();
|
||||
Get.offAll(() => const MapPagePassenger());
|
||||
}
|
||||
// ... (باقي الحالات مثل Call Income, Call End, إلخ) ...
|
||||
// ... بنفس الطريقة ...
|
||||
@@ -617,7 +585,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
Future<dynamic> passengerDialog(String message) {
|
||||
return Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'message From Driver'.tr,
|
||||
title: message.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
middleTextStyle: AppStyle.title,
|
||||
middleText: message.tr,
|
||||
|
||||
@@ -1,41 +1,44 @@
|
||||
import 'package:Intaleq/print.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:get/get.dart'; // للترجمة .tr
|
||||
|
||||
class NotificationService {
|
||||
// استبدل هذا الرابط بالرابط الصحيح لملف PHP على السيرفر الخاص بك
|
||||
static const String _serverUrl =
|
||||
'https://syria.intaleq.xyz/intaleq/fcm/send_fcm.php';
|
||||
static const String _batchServerUrl =
|
||||
'https://syria.intaleq.xyz/intaleq/fcm/send_fcm_batch.php';
|
||||
'https://api.intaleq.xyz/intaleq/ride/firebase/send_fcm.php';
|
||||
|
||||
static Future<void> sendNotification({
|
||||
required String target,
|
||||
required String title,
|
||||
required String body,
|
||||
required String? category, // <-- [الإضافة الأولى]
|
||||
required String category, // إلزامي للتصنيف
|
||||
String? tone,
|
||||
List<String>? driverList, // <-- [تعديل 1] : إضافة المتغير الجديد
|
||||
List<String>? driverList,
|
||||
bool isTopic = false,
|
||||
}) async {
|
||||
try {
|
||||
final Map<String, dynamic> payload = {
|
||||
// 1. تجهيز البيانات المخصصة (Data Payload)
|
||||
Map<String, dynamic> customData = {};
|
||||
|
||||
customData['category'] = category;
|
||||
|
||||
// إذا كان هناك قائمة سائقين/ركاب، نضعها هنا
|
||||
if (driverList != null && driverList.isNotEmpty) {
|
||||
// نرسلها كـ JSON String لأن FCM v1 يدعم String Values فقط في الـ data
|
||||
customData['driverList'] = jsonEncode(driverList);
|
||||
}
|
||||
|
||||
// 2. تجهيز الطلب الرئيسي للسيرفر
|
||||
final Map<String, dynamic> requestPayload = {
|
||||
'target': target,
|
||||
'title': title,
|
||||
'body': body,
|
||||
'isTopic': isTopic,
|
||||
'data':
|
||||
customData, // 🔥🔥 التغيير الجوهري: وضعنا البيانات داخل "data" 🔥🔥
|
||||
};
|
||||
if (category != null) {
|
||||
payload['category'] =
|
||||
category; // <-- [الإضافة الثانية] (النص الثابت للتحكم)
|
||||
}
|
||||
// نضيف النغمة فقط إذا لم تكن فارغة
|
||||
if (tone != null) {
|
||||
payload['tone'] = tone;
|
||||
}
|
||||
|
||||
// <-- [تعديل 2] : نضيف قائمة البيانات بعد تشفيرها إلى JSON
|
||||
if (driverList != null) {
|
||||
payload['driverList'] = jsonEncode(driverList);
|
||||
if (tone != null) {
|
||||
requestPayload['tone'] = tone;
|
||||
}
|
||||
|
||||
final response = await http.post(
|
||||
@@ -43,71 +46,18 @@ class NotificationService {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
body: jsonEncode(payload),
|
||||
body: jsonEncode(requestPayload),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print('✅ Notification sent successfully.');
|
||||
print('Server Response: ${response.body}');
|
||||
// print('Response: ${response.body}');
|
||||
} else {
|
||||
print(
|
||||
'❌ Failed to send notification. Status code: ${response.statusCode}');
|
||||
print('Server Error: ${response.body}');
|
||||
print('❌ Failed to send notification. Code: ${response.statusCode}');
|
||||
print('Error Body: ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ An error occurred while sending notification: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// [4] !! دالة جديدة مضافة !!
|
||||
/// ترسل إشعاراً "مجمعاً" إلى قائمة من السائقين
|
||||
static Future<void> sendBatchNotification({
|
||||
required List<String> targets, // <-- قائمة التوكينز
|
||||
required String title,
|
||||
required String body,
|
||||
String? tone,
|
||||
List<String>? driverList, // <-- بيانات الرحلة (نفسها للجميع)
|
||||
}) async {
|
||||
// لا ترسل شيئاً إذا كانت القائمة فارغة
|
||||
if (targets.isEmpty) {
|
||||
Log.print('⚠️ [Batch] No targets to send to. Skipped.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final Map<String, dynamic> payload = {
|
||||
// "targets" بدلاً من "target"
|
||||
'targets': jsonEncode(targets), // تشفير قائمة التوكينز
|
||||
'title': title,
|
||||
'body': body,
|
||||
};
|
||||
|
||||
if (tone != null) {
|
||||
payload['tone'] = tone;
|
||||
}
|
||||
|
||||
// بيانات الرحلة (DriverList)
|
||||
if (driverList != null) {
|
||||
payload['driverList'] = jsonEncode(driverList);
|
||||
}
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse(_batchServerUrl), // <-- !! تستخدم الرابط الجديد
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
body: jsonEncode(payload),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
Log.print('✅ [Batch] Notifications sent successfully.');
|
||||
Log.print('Server Response: ${response.body}');
|
||||
} else {
|
||||
Log.print('❌ [Batch] Failed to send. Status: ${response.statusCode}');
|
||||
Log.print('Server Error: ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('❌ [Batch] An error occurred: $e');
|
||||
print('❌ Error sending notification: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class AudioRecorderController extends GetxController {
|
||||
|
||||
// Stop recording
|
||||
Future<void> stopRecording() async {
|
||||
await recorder.stop();
|
||||
recorder.stop();
|
||||
isRecording = false;
|
||||
isPaused = false;
|
||||
update();
|
||||
|
||||
@@ -100,9 +100,8 @@ class CRUD {
|
||||
}
|
||||
|
||||
final sc = response.statusCode;
|
||||
Log.print('sc: ${sc}');
|
||||
Log.print('request: ${response.request}');
|
||||
final body = response.body;
|
||||
Log.print('request: ${response.request}');
|
||||
Log.print('body: ${body}');
|
||||
|
||||
// 2xx
|
||||
@@ -188,9 +187,9 @@ class CRUD {
|
||||
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}'
|
||||
},
|
||||
);
|
||||
Log.print('response.body: ${response.body}');
|
||||
Log.print('response.request: ${response.request}');
|
||||
Log.print('response.payload: ${payload}');
|
||||
Log.print('request: ${response.request}');
|
||||
Log.print('body: ${response.body}');
|
||||
Log.print('payload: ${payload}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
|
||||
@@ -11,19 +11,17 @@ Future<void> makePhoneCall(String phoneNumber) async {
|
||||
// 1. تنظيف الرقم (إزالة المسافات والفواصل)
|
||||
String formattedNumber = phoneNumber.replaceAll(RegExp(r'\s+'), '');
|
||||
|
||||
// 2. التحقق من طول الرقم لتحديد طريقة التنسيق
|
||||
// 2. منطق التنسيق (مع الحفاظ على الأرقام القصيرة مثل 112 كما هي)
|
||||
if (formattedNumber.length > 6) {
|
||||
// --- التعديل المطلوب ---
|
||||
if (formattedNumber.startsWith('09')) {
|
||||
// إذا كان يبدأ بـ 09 (رقم موبايل سوري محلي)
|
||||
// نحذف أول خانة (الصفر) ونضيف +963
|
||||
// إذا كان يبدأ بـ 09 (رقم موبايل سوري محلي) -> +963
|
||||
formattedNumber = '+963${formattedNumber.substring(1)}';
|
||||
} else if (!formattedNumber.startsWith('+')) {
|
||||
// إذا لم يكن يبدأ بـ + (ولم يكن يبدأ بـ 09)، نضيف + في البداية
|
||||
// هذا للحفاظ على منطقك القديم للأرقام الدولية الأخرى
|
||||
// إذا لم يكن دولياً ولا محلياً معروفاً -> إضافة + فقط
|
||||
formattedNumber = '+$formattedNumber';
|
||||
}
|
||||
}
|
||||
// ملاحظة: الأرقام القصيرة (مثل 112) ستتجاوز الشرط أعلاه وتبقى "112" وهو الصحيح
|
||||
|
||||
// 3. التنفيذ (Launch)
|
||||
final Uri launchUri = Uri(
|
||||
@@ -31,8 +29,19 @@ Future<void> makePhoneCall(String phoneNumber) async {
|
||||
path: formattedNumber,
|
||||
);
|
||||
|
||||
if (await canLaunchUrl(launchUri)) {
|
||||
await launchUrl(launchUri);
|
||||
try {
|
||||
// استخدام LaunchMode.externalApplication هو الحل الجذري لمشاكل الـ Intent
|
||||
// لأنه يجبر النظام على تسليم الرابط لتطبيق الهاتف بدلاً من محاولة فتحه داخل تطبيقك
|
||||
if (await canLaunchUrl(launchUri)) {
|
||||
await launchUrl(launchUri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
// في بعض الأجهزة canLaunchUrl تعود بـ false مع الـ tel ومع ذلك يعمل launchUrl
|
||||
// لذا نجرب الإطلاق المباشر كاحتياط
|
||||
await launchUrl(launchUri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
} catch (e) {
|
||||
// طباعة الخطأ في حال الفشل التام
|
||||
print("Error launching call: $e");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
127
lib/controller/home/device_performance.dart
Normal file
127
lib/controller/home/device_performance.dart
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
|
||||
class DevicePerformanceManager {
|
||||
/// القائمة البيضاء لموديلات الهواتف القوية (Flagships Only)
|
||||
/// أي هاتف يبدأ موديله بأحد هذه الرموز سيعتبر قوياً
|
||||
static const List<String> _highEndSamsungModels = [
|
||||
'SM-S', // سلسلة Galaxy S21, S22, S23, S24 (S901, S908, S911...)
|
||||
'SM-F', // سلسلة Fold و Flip (Z Fold, Z Flip)
|
||||
'SM-N9', // سلسلة Note 9, Note 10, Note 20
|
||||
'SM-G9', // سلسلة S10, S20 (G970, G980...)
|
||||
];
|
||||
|
||||
static const List<String> _highEndGoogleModels = [
|
||||
'Pixel 6',
|
||||
'Pixel 7',
|
||||
'Pixel 8',
|
||||
'Pixel 9',
|
||||
'Pixel Fold'
|
||||
];
|
||||
|
||||
static const List<String> _highEndHuaweiModels = [
|
||||
'ELS-', // P40 Pro
|
||||
'ANA-', // P40
|
||||
'HMA-', // Mate 20
|
||||
'LYA-', // Mate 20 Pro
|
||||
'VOG-', // P30 Pro
|
||||
'ELE-', // P30
|
||||
'NOH-', // Mate 40 Pro
|
||||
'AL00', // Mate X series (some)
|
||||
];
|
||||
|
||||
static const List<String> _highEndXiaomiModels = [
|
||||
'2201122', // Xiaomi 12 series patterns often look like this
|
||||
'2210132', // Xiaomi 13
|
||||
'2304FPN', // Xiaomi 13 Ultra
|
||||
'M2007J1', // Mi 10 series
|
||||
'M2102K1', // Mi 11 Ultra
|
||||
];
|
||||
|
||||
static const List<String> _highEndOnePlusModels = [
|
||||
'GM19', // OnePlus 7
|
||||
'HD19', // OnePlus 7T
|
||||
'IN20', // OnePlus 8
|
||||
'KB20', // OnePlus 8T
|
||||
'LE21', // OnePlus 9
|
||||
'NE22', // OnePlus 10
|
||||
'PHB110', // OnePlus 11
|
||||
'CPH', // Newer OnePlus models
|
||||
];
|
||||
|
||||
/// دالة الفحص الرئيسية
|
||||
static Future<bool> isHighEndDevice() async {
|
||||
// 1. الآيفون دائماً قوي (نظام الرسوميات فيه متفوق حتى في الموديلات القديمة)
|
||||
if (Platform.isIOS) return true;
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
|
||||
String manufacturer = androidInfo.manufacturer.toLowerCase();
|
||||
String model =
|
||||
androidInfo.model.toUpperCase(); // نحوله لحروف كبيرة للمقارنة
|
||||
String hardware = androidInfo.hardware.toLowerCase(); // المعالج
|
||||
|
||||
// --- الفحص العكسي (الحظر المباشر) ---
|
||||
// إذا كان المعالج من الفئات الضعيفة جداً المشهورة في الهواتف المقلدة
|
||||
// mt65xx, mt6735, sc77xx هي معالجات رخيصة جداً
|
||||
if (hardware.contains('mt65') ||
|
||||
hardware.contains('mt6735') ||
|
||||
hardware.contains('sc77')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- فحص القائمة البيضاء (Whitelist) ---
|
||||
|
||||
// 1. Samsung Flagships
|
||||
if (manufacturer.contains('samsung')) {
|
||||
for (var prefix in _highEndSamsungModels) {
|
||||
if (model.startsWith(prefix)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Google Pixel (6 and above)
|
||||
if (manufacturer.contains('google')) {
|
||||
for (var prefix in _highEndGoogleModels) {
|
||||
if (model.contains(prefix.toUpperCase())) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Huawei Flagships
|
||||
if (manufacturer.contains('huawei')) {
|
||||
for (var prefix in _highEndHuaweiModels) {
|
||||
if (model.startsWith(prefix)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. OnePlus Flagships
|
||||
if (manufacturer.contains('oneplus')) {
|
||||
for (var prefix in _highEndOnePlusModels) {
|
||||
if (model.startsWith(prefix)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Xiaomi Flagships
|
||||
if (manufacturer.contains('xiaomi') ||
|
||||
manufacturer.contains('redmi') ||
|
||||
manufacturer.contains('poco')) {
|
||||
// شاومي تسميتها معقدة، لذا سنعتمد على فحص الرام كعامل مساعد هنا فقط
|
||||
// لأن هواتف شاومي القوية عادة لا تزور الرام
|
||||
// الرام يجب أن يكون أكبر من 6 جيجا (بايت)
|
||||
double ramGB = (androidInfo.availableRamSize) / (1024 * 1024 * 1024);
|
||||
if (ramGB > 7.5)
|
||||
return true; // 8GB RAM or more is usually safe for Xiaomi high-end
|
||||
}
|
||||
|
||||
// إذا لم يكن من ضمن القوائم أعلاه، نعتبره جهازاً متوسطاً/ضعيفاً ونعرض الرسم البسيط
|
||||
return false;
|
||||
} catch (e) {
|
||||
// في حال حدوث خطأ في الفحص، نعود للوضع الآمن (الرسم البسيط)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,7 @@ class RateController extends GetxController {
|
||||
update();
|
||||
}
|
||||
|
||||
void addRateToDriver() async {
|
||||
addRateToDriver() async {
|
||||
if (selectedRateItemId < 1) {
|
||||
Get.defaultDialog(
|
||||
title: 'You Should choose rate figure'.tr,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:Intaleq/views/home/map_page_passenger.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -148,10 +149,11 @@ class RatingDriverBottomSheet extends StatelessWidget {
|
||||
// 4. زر الإرسال
|
||||
MyElevatedButton(
|
||||
title: 'Submit Rating'.tr,
|
||||
onPressed: () {
|
||||
controller.addRateToDriver();
|
||||
Get.find<MapPassengerController>()
|
||||
.getRideStatusFromStartApp();
|
||||
onPressed: () async {
|
||||
await controller.addRateToDriver();
|
||||
Get.offAll(() => MapPagePassenger());
|
||||
// Get.find<MapPassengerController>()
|
||||
// .getRideStatusFromStartApp();
|
||||
})
|
||||
],
|
||||
),
|
||||
|
||||
@@ -35,6 +35,9 @@ class AuthScreen extends StatelessWidget {
|
||||
final testerPasswordController = TextEditingController();
|
||||
final testerFormKey = GlobalKey<FormState>();
|
||||
|
||||
// Brand Color for Logic (Cyan/Teal from the Arrow in the logo)
|
||||
const Color brandColor = Color(0xFF00E5FF);
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
@@ -42,7 +45,8 @@ class AuthScreen extends StatelessWidget {
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||
child: AlertDialog(
|
||||
backgroundColor: const Color(0xFF162232).withOpacity(0.85),
|
||||
// Updated background to match new theme (Dark Purple/Indigo)
|
||||
backgroundColor: const Color(0xFF1A1A2E).withOpacity(0.90),
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: const Text(
|
||||
@@ -73,7 +77,8 @@ class AuthScreen extends StatelessWidget {
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Color(0xFF00BFFF)),
|
||||
// Changed to Brand Cyan
|
||||
borderSide: const BorderSide(color: brandColor),
|
||||
),
|
||||
),
|
||||
validator: (value) => value == null || !value.contains('@')
|
||||
@@ -98,7 +103,8 @@ class AuthScreen extends StatelessWidget {
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Color(0xFF00BFFF)),
|
||||
// Changed to Brand Cyan
|
||||
borderSide: const BorderSide(color: brandColor),
|
||||
),
|
||||
),
|
||||
validator: (value) => value == null || value.isEmpty
|
||||
@@ -116,13 +122,14 @@ class AuthScreen extends StatelessWidget {
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF00BFFF),
|
||||
backgroundColor: brandColor, // Updated Button Color
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child:
|
||||
const Text('Login', style: TextStyle(color: Colors.black)),
|
||||
child: const Text('Login',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF1A1A2E), fontWeight: FontWeight.bold)),
|
||||
onPressed: () {
|
||||
if (testerFormKey.currentState!.validate()) {
|
||||
// Use the main controller to perform login
|
||||
@@ -149,39 +156,58 @@ class AuthScreen extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
// NEW: AI-inspired, brighter, and more dynamic color gradient
|
||||
// NEW DESIGN: Deep Purple/Indigo Gradient to match the "N" body
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Color(0xFF00122E), Color(0xFF00285F)],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
// Dark Indigo -> Deep Purple -> Dark Blue
|
||||
colors: [Color(0xFF2E1C59), Color(0xFF1A237E), Color(0xFF0D1117)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Background shapes for a more dynamic feel
|
||||
// Background shapes updated to match the Logo accents
|
||||
|
||||
// Shape 1: The Orange/Red Swoosh color
|
||||
Positioned(
|
||||
top: -100,
|
||||
left: -100,
|
||||
top: -80,
|
||||
left: -80,
|
||||
child: Container(
|
||||
width: 250,
|
||||
height: 250,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0xFF00BFFF).withOpacity(0.15),
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
// Orange/Red from the swoosh lines
|
||||
color: const Color(0xFFFF5722).withOpacity(0.12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFFFF5722).withOpacity(0.2),
|
||||
blurRadius: 50,
|
||||
spreadRadius: 10,
|
||||
)
|
||||
]),
|
||||
),
|
||||
),
|
||||
|
||||
// Shape 2: The Cyan/Teal Arrow color
|
||||
Positioned(
|
||||
bottom: -150,
|
||||
right: -100,
|
||||
bottom: -100,
|
||||
right: -80,
|
||||
child: Container(
|
||||
width: 350,
|
||||
height: 350,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0xFF00BFFF).withOpacity(0.1),
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
// Cyan/Teal from the arrow tip
|
||||
color: const Color(0xFF00E5FF).withOpacity(0.08),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF00E5FF).withOpacity(0.15),
|
||||
blurRadius: 60,
|
||||
spreadRadius: 5,
|
||||
)
|
||||
]),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
@@ -199,10 +225,11 @@ class AuthScreen extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
color: Colors.white.withOpacity(0.05),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
width: 2)),
|
||||
// Gradient border for the logo container
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
width: 1)),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: Image.asset('assets/images/logo.gif',
|
||||
@@ -219,9 +246,9 @@ class AuthScreen extends StatelessWidget {
|
||||
color: Colors.white,
|
||||
shadows: [
|
||||
Shadow(
|
||||
blurRadius: 10.0,
|
||||
color: Colors.black26,
|
||||
offset: Offset(2, 2)),
|
||||
blurRadius: 15.0,
|
||||
color: Color(0xFF000000), // Darker shadow
|
||||
offset: Offset(0, 4)),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
@@ -230,7 +257,7 @@ class AuthScreen extends StatelessWidget {
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
color: Colors.white.withOpacity(0.75),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
@@ -239,15 +266,17 @@ class AuthScreen extends StatelessWidget {
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(25.0),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 20, sigmaY: 20), // Increased blur
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
// Slightly darker tint for better contrast with Cyan inputs
|
||||
color: const Color(0xFF1A237E).withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(25.0),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
width: 1.5,
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
child:
|
||||
@@ -258,8 +287,7 @@ class AuthScreen extends StatelessWidget {
|
||||
const SizedBox(height: 20),
|
||||
// A more distinct button for app testers
|
||||
Material(
|
||||
color: Colors.white.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
_showTesterLoginDialog(context, loginController),
|
||||
@@ -271,14 +299,14 @@ class AuthScreen extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.admin_panel_settings_outlined,
|
||||
color: Colors.white.withOpacity(0.8)),
|
||||
color: Colors.white.withOpacity(0.5)),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'For App Reviewers / Testers',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withOpacity(0.8),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -310,6 +338,10 @@ class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
|
||||
final _phoneController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = false;
|
||||
|
||||
// Brand Color for Focus (Cyan/Teal)
|
||||
final Color _focusColor = const Color(0xFF00E5FF);
|
||||
|
||||
static String formatSyrianPhone(String phone) {
|
||||
// Remove spaces, symbols, +, -, ()
|
||||
phone = phone.replaceAll(RegExp(r'[ \-\(\)\+]'), '').trim();
|
||||
@@ -404,7 +436,9 @@ class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
|
||||
searchText: 'Search country'.tr,
|
||||
languageCode: 'ar',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
dropdownTextStyle: const TextStyle(color: Colors.black87),
|
||||
dropdownTextStyle: const TextStyle(
|
||||
color: Colors
|
||||
.white), // Changed to White for visibility on dark BG
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Phone Number'.tr,
|
||||
hintText: 'witout zero'.tr,
|
||||
@@ -415,7 +449,8 @@ class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Color(0xFF00BFFF)),
|
||||
// Updated to Logo Cyan
|
||||
borderSide: BorderSide(color: _focusColor, width: 2),
|
||||
),
|
||||
),
|
||||
initialCountryCode: 'SY',
|
||||
@@ -444,8 +479,11 @@ class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF00BFFF),
|
||||
// Updated to Logo Cyan
|
||||
backgroundColor: _focusColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
elevation: 5,
|
||||
shadowColor: _focusColor.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
@@ -454,7 +492,8 @@ class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black),
|
||||
// Text is dark to contrast with bright Cyan
|
||||
color: Color(0xFF1A1A2E)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -477,6 +516,9 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||
final _otpController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
// Brand Color
|
||||
final Color _brandColor = const Color(0xFF00E5FF);
|
||||
|
||||
void _submit() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
setState(() => _isLoading = true);
|
||||
@@ -519,11 +561,17 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||
counterText: "",
|
||||
hintText: '-----',
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
letterSpacing: 18,
|
||||
fontSize: 28),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 10),
|
||||
// Add a subtle underline for the OTP area using brand color
|
||||
enabledBorder: UnderlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Colors.white.withOpacity(0.2))),
|
||||
focusedBorder: UnderlineInputBorder(
|
||||
borderSide: BorderSide(color: _brandColor)),
|
||||
),
|
||||
validator: (v) => v == null || v.length < 5 ? '' : null,
|
||||
),
|
||||
@@ -536,8 +584,10 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF00BFFF),
|
||||
backgroundColor: _brandColor, // Updated
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
elevation: 5,
|
||||
shadowColor: _brandColor.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
@@ -546,7 +596,7 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black),
|
||||
color: Color(0xFF1A1A2E)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -570,6 +620,9 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
final _emailController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
// Brand Color
|
||||
final Color _brandColor = const Color(0xFF00E5FF);
|
||||
|
||||
void _submit() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
setState(() => _isLoading = true);
|
||||
@@ -603,7 +656,8 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Color(0xFF00BFFF)),
|
||||
// Updated to Logo Cyan
|
||||
borderSide: BorderSide(color: _brandColor, width: 2),
|
||||
),
|
||||
),
|
||||
keyboardType: keyboardType,
|
||||
@@ -646,8 +700,10 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF00BFFF),
|
||||
backgroundColor: _brandColor, // Updated
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
elevation: 5,
|
||||
shadowColor: _brandColor.withOpacity(0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
@@ -656,7 +712,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black),
|
||||
color: Color(0xFF1A1A2E)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -39,7 +39,7 @@ class MapPagePassenger extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
bottom: false,
|
||||
bottom: true,
|
||||
child: Stack(
|
||||
children: [
|
||||
GoogleMapPassengerWidget(),
|
||||
@@ -89,17 +89,34 @@ class CancelRidePageShow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
builder: (controller) => (controller.polyLines.isNotEmpty &&
|
||||
controller.statusRide != 'Begin')
|
||||
// ||
|
||||
// controller.timeToPassengerFromDriverAfterApplied == 0
|
||||
builder: (controller) {
|
||||
// نستخدم RideState Enum لأنه أدق، أو نصلح المنطق النصي
|
||||
// الشرط:
|
||||
// 1. يوجد خط مسار
|
||||
// 2. الحالة ليست "بدأت"
|
||||
// 3. الحالة ليست "انتهت"
|
||||
// 4. الحالة ليست "قيد التنفيذ" (لزيادة التأكيد)
|
||||
|
||||
// bool showCancelButton = controller.polyLines.isNotEmpty &&
|
||||
// controller.statusRide != 'Begin' && // استخدمنا &&
|
||||
// controller.statusRide != 'inProgress' &&
|
||||
// controller.statusRide != 'Finished';
|
||||
|
||||
// يمكنك أيضاً استخدام RideState ليكون أدق:
|
||||
bool showCancelButton = controller.polyLines.isNotEmpty &&
|
||||
controller.currentRideState.value != RideState.inProgress &&
|
||||
controller.currentRideState.value != RideState.finished;
|
||||
|
||||
return showCancelButton
|
||||
? Positioned(
|
||||
right: box.read(BoxName.lang) != 'ar' ? 10 : null,
|
||||
left: box.read(BoxName.lang) == 'ar' ? 10 : null,
|
||||
top: Get.height * .013,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
// استدعاء دالة الإلغاء
|
||||
controller.changeCancelRidePageShow();
|
||||
// ملاحظة: تأكد أن الدالة تظهر ديالوج للتأكيد أولاً ولا تلغي فوراً
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -115,7 +132,9 @@ class CancelRidePageShow extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
))
|
||||
: const SizedBox());
|
||||
: const SizedBox();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,20 +5,18 @@ import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart'; // لتنسيق الأرقام
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../controller/firebase/notification_service.dart';
|
||||
import '../../../controller/functions/launch.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
|
||||
class ApplyOrderWidget extends StatelessWidget {
|
||||
const ApplyOrderWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// دالة لتحويل كود اللون الهيكس إلى لون
|
||||
Color parseColor(String colorHex) {
|
||||
if (colorHex.isEmpty) return Colors.grey;
|
||||
try {
|
||||
@@ -39,57 +37,59 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.elasticOut, // تأثير حركي أجمل
|
||||
bottom: isVisible ? 0 : -Get.height * 0.6,
|
||||
curve: Curves.elasticOut,
|
||||
// تغيير: جعلنا الإخفاء للأسفل أقل حدة ليكون التحريك أسرع
|
||||
bottom: isVisible ? 0 : -400,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
// height: Get.height * 0.38, // زيادة الارتفاع قليلاً للتصميم الجديد
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(25)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
blurRadius: 20,
|
||||
spreadRadius: 2,
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
offset: const Offset(0, -2),
|
||||
spreadRadius: 1,
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
offset: const Offset(0, -3),
|
||||
)
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
|
||||
// تغيير: تقليل الحواف الخارجية بشكل كبير
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: GetBuilder<MapPassengerController>(
|
||||
builder: (c) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisSize:
|
||||
MainAxisSize.min, // مهم جداً: يأخذ أقل مساحة ممكنة
|
||||
children: [
|
||||
// مقبض صغير في الأعلى
|
||||
// مقبض صغير
|
||||
Container(
|
||||
width: 40,
|
||||
height: 5,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
const SizedBox(height: 10), // تقليل المسافة
|
||||
|
||||
// السعر والعنوان
|
||||
_buildPriceHeader(context, c),
|
||||
// 1. [تغيير جوهري] دمج السعر مع الحالة في صف واحد لتوفير المساحة
|
||||
_buildCompactHeaderRow(context, c),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
const SizedBox(height: 10), // مسافة مضغوطة
|
||||
|
||||
// كرت المعلومات الرئيسي (سائق + سيارة)
|
||||
_buildMainInfoCard(context, c, parseColor),
|
||||
// 2. كرت المعلومات المضغوط
|
||||
_buildCompactInfoCard(context, c, parseColor),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
const SizedBox(height: 10), // مسافة مضغوطة
|
||||
|
||||
// أزرار الاتصال
|
||||
_buildContactButtonsRow(context, c),
|
||||
// 3. أزرار الاتصال (Slim)
|
||||
_buildCompactButtonsRow(context, c),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
const SizedBox(height: 10), // مسافة مضغوطة
|
||||
|
||||
// شريط الوقت
|
||||
// 4. شريط الوقت
|
||||
c.currentRideState.value == RideState.driverArrived
|
||||
? const DriverArrivePassengerAndWaitMinute()
|
||||
: const TimeDriverToPassenger(),
|
||||
@@ -103,42 +103,90 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. قسم السعر (مع التنسيق الجديد)
|
||||
// [NEW] 1. صف الرأس المضغوط (يحتوي الحالة + الإحصائيات + السعر)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildPriceHeader(
|
||||
Widget _buildCompactHeaderRow(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
// تنسيق الرقم (مثلاً: 60,000)
|
||||
// تنسيق السعر
|
||||
final formatter = NumberFormat("#,###");
|
||||
String formattedPrice = formatter.format(controller.totalPassenger);
|
||||
|
||||
return Column(
|
||||
// حساب الدقائق
|
||||
int minutes =
|
||||
(controller.timeToPassengerFromDriverAfterApplied / 60).ceil();
|
||||
if (minutes < 1) minutes = 1;
|
||||
|
||||
// تنسيق المسافة
|
||||
String distanceDisplay = "";
|
||||
try {
|
||||
double distMeters = double.parse(controller.distanceByPassenger);
|
||||
if (distMeters >= 1000) {
|
||||
distanceDisplay = "${(distMeters / 1000).toStringAsFixed(1)} km";
|
||||
} else {
|
||||
distanceDisplay = "${distMeters.toInt()} m";
|
||||
}
|
||||
} catch (e) {
|
||||
distanceDisplay = controller.distanceByPassenger;
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Driver Accepted Request'.tr,
|
||||
style: AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
||||
// القسم الأيسر: الحالة + Chips
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Driver is on the way'.tr,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: Colors.grey[600],
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13, // تصغير الخط
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
_buildMiniStatChip(
|
||||
icon: Icons.access_time_filled_rounded,
|
||||
text: "$minutes ${'min'.tr}",
|
||||
color: AppColor.primaryColor,
|
||||
bgColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildMiniStatChip(
|
||||
icon: Icons.near_me_rounded,
|
||||
text: distanceDisplay,
|
||||
color: Colors.orange[800]!,
|
||||
bgColor: Colors.orange.withOpacity(0.1),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
// القسم الأيمن: السعر (كبير وواضح في الزاوية)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
formattedPrice,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 28,
|
||||
fontSize: 24, // تصغير من 32 إلى 24
|
||||
fontWeight: FontWeight.w900,
|
||||
color: AppColor.primaryColor,
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'SYP'.tr,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
Text(
|
||||
'SYP'.tr,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -147,207 +195,251 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMiniStatChip({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
required Color color,
|
||||
required Color bgColor,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 12, color: color), // تصغير الأيقونة
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12, // تصغير الخط
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. كرت المعلومات الرئيسي (السائق + السيارة 3D)
|
||||
// [MODIFIED] 2. كرت المعلومات المضغوط جداً
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildMainInfoCard(BuildContext context,
|
||||
Widget _buildCompactInfoCard(BuildContext context,
|
||||
MapPassengerController controller, Color Function(String) parseColor) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
// تقليل الحواف الداخلية للكرت
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor, // لون خلفية فاتح
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// الصف العلوي: سائق + سيارة
|
||||
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),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// معلومات نصية
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
controller.driverName,
|
||||
style: const TextStyle(
|
||||
fontSize: 15, // تصغير الخط
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.star_rounded,
|
||||
color: Colors.amber, size: 14),
|
||||
Text(
|
||||
" ${controller.driverRate} • ${controller.model}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// أيقونة السيارة (أصغر)
|
||||
_buildMicroCarIcon(controller, parseColor),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// لوحة السيارة (شريط نحيف جداً)
|
||||
_buildSlimLicensePlate(controller.licensePlate),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMicroCarIcon(
|
||||
MapPassengerController controller, Color Function(String) parseColor) {
|
||||
Color carColor = parseColor(controller.colorHex);
|
||||
return Container(
|
||||
height: 40, // تصغير من 50
|
||||
width: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: carColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(carColor, BlendMode.srcIn),
|
||||
child: Image.asset(
|
||||
box.read(BoxName.carType) == 'Scooter' ||
|
||||
box.read(BoxName.carType) == 'Pink Bike'
|
||||
? 'assets/images/moto.png'
|
||||
: 'assets/images/car3.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSlimLicensePlate(String plateNumber) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// الجزء الأيسر: معلومات السائق
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// صورة السائق
|
||||
Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColor.primaryColor, width: 2),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 26,
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'),
|
||||
onBackgroundImageError: (exception, stackTrace) =>
|
||||
const Icon(Icons.person, size: 26, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// الاسم والتقييم والسيارة نص
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
controller.driverName,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 16, fontWeight: FontWeight.bold),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.star, color: Colors.amber, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
controller.driverRate,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${controller.model} • ${controller.licensePlate}',
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 12),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
Text(
|
||||
plateNumber,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'RobotoMono',
|
||||
fontSize: 18, // تصغير الرقم
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.black87,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
|
||||
// الجزء الأيمن: أيقونة السيارة الـ 3D
|
||||
_build3DCarIcon(controller, parseColor),
|
||||
const Text("SYR",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black54)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. أيقونة السيارة الـ 3D (الدائرة والظلال والخلفية الذكية)
|
||||
// [MODIFIED] 3. أزرار الاتصال (Slim Buttons)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _build3DCarIcon(
|
||||
MapPassengerController controller, Color Function(String) parseColor) {
|
||||
Color carColor = parseColor(controller.colorHex);
|
||||
|
||||
// تحديد سطوع لون السيارة لتحديد لون الخلفية
|
||||
// إذا كانت السيارة فاتحة (أكثر من 0.6)، الخلفية تكون غامقة، والعكس
|
||||
bool isCarLight = carColor.computeLuminance() > 0.6;
|
||||
|
||||
// ألوان الخلفية للدائرة
|
||||
Color bgGradientStart =
|
||||
isCarLight ? Colors.blueGrey.shade700 : Colors.grey.shade100;
|
||||
Color bgGradientEnd =
|
||||
isCarLight ? Colors.blueGrey.shade900 : Colors.grey.shade300;
|
||||
Color borderColor = isCarLight ? Colors.blueGrey.shade600 : Colors.white;
|
||||
|
||||
return Container(
|
||||
width: 75,
|
||||
height: 75,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
// تدرج لوني للخلفية لتبدو 3D
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [bgGradientStart, bgGradientEnd],
|
||||
),
|
||||
border: Border.all(color: borderColor, width: 2),
|
||||
// ظلال لرفع الدائرة عن السطح
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(4, 4),
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.white.withOpacity(isCarLight ? 0.1 : 0.8),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(-4, -4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: ColorFiltered(
|
||||
colorFilter: ColorFilter.mode(carColor, BlendMode.srcIn),
|
||||
child: Image.asset(
|
||||
box.read(BoxName.carType) == 'Scooter' ||
|
||||
box.read(BoxName.carType) == 'Pink Bike'
|
||||
? 'assets/images/moto.png'
|
||||
: 'assets/images/car3.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4. أزرار الاتصال (بتصميم جديد)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildContactButtonsRow(
|
||||
Widget _buildCompactButtonsRow(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildActionButton(
|
||||
label: 'Message'.tr,
|
||||
icon: Icons.chat_bubble_outline_rounded,
|
||||
color: AppColor.blueColor,
|
||||
onTap: () => _showContactOptionsDialog(context, controller),
|
||||
return SizedBox(
|
||||
height: 40, // تحديد ارتفاع ثابت وصغير للأزرار
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildSlimButton(
|
||||
label: 'Message'.tr, // اختصار الكلمة
|
||||
icon: Icons.chat_bubble_outline_rounded,
|
||||
color: AppColor.blueColor,
|
||||
bgColor: AppColor.blueColor.withOpacity(0.08),
|
||||
onTap: () => _showContactOptionsDialog(context, controller),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: _buildActionButton(
|
||||
label: 'Call'.tr,
|
||||
icon: Icons.phone_rounded,
|
||||
color: AppColor.greenColor,
|
||||
onTap: () {
|
||||
HapticFeedback.heavyImpact();
|
||||
makePhoneCall(controller.driverPhone);
|
||||
},
|
||||
const SizedBox(width: 10), // تقليل المسافة
|
||||
Expanded(
|
||||
child: _buildSlimButton(
|
||||
label: 'Call'.tr, // اختصار الكلمة
|
||||
icon: Icons.phone_rounded,
|
||||
color: Colors.white,
|
||||
bgColor: AppColor.greenColor,
|
||||
onTap: () {
|
||||
HapticFeedback.heavyImpact();
|
||||
makePhoneCall(controller.driverPhone);
|
||||
},
|
||||
isPrimary: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
Widget _buildSlimButton({
|
||||
required String label,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required Color bgColor,
|
||||
required VoidCallback onTap,
|
||||
bool isPrimary = false,
|
||||
}) {
|
||||
return ElevatedButton(
|
||||
onPressed: onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: color.withOpacity(0.1),
|
||||
backgroundColor: bgColor,
|
||||
foregroundColor: color,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: isPrimary ? 2 : 0,
|
||||
padding: EdgeInsets.zero, // إزالة الحواشي الداخلية
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Icon(icon, size: 18, color: color), // تصغير الأيقونة
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14, // تصغير الخط
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- النوافذ المنبثقة للرسائل (نفس المنطق القديم) ---
|
||||
// --- النوافذ المنبثقة للرسائل (نفس الكود السابق مع تحسين بسيط) ---
|
||||
void _showContactOptionsDialog(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
Get.bottomSheet(
|
||||
@@ -361,13 +453,13 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Quick Message'.tr, style: AppStyle.title),
|
||||
Text('Quick Message'.tr,
|
||||
style: AppStyle.title.copyWith(fontSize: 16)),
|
||||
const SizedBox(height: 15),
|
||||
..._buildPredefinedMessages(controller),
|
||||
const Divider(height: 30),
|
||||
const Divider(height: 20),
|
||||
_buildCustomMessageInput(controller, context),
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).viewInsets.bottom), // للكيبورد
|
||||
SizedBox(height: MediaQuery.of(context).viewInsets.bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -384,7 +476,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
|
||||
return messages
|
||||
.map((message) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10.0),
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_sendMessage(controller, message.tr);
|
||||
@@ -392,18 +484,20 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
},
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 12, horizontal: 15),
|
||||
const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.quickreply_rounded,
|
||||
size: 18, color: Colors.grey),
|
||||
Icon(Icons.chat_bubble_outline,
|
||||
size: 16, color: AppColor.primaryColor),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(message.tr, style: AppStyle.subtitle)),
|
||||
child: Text(message.tr,
|
||||
style: AppStyle.subtitle.copyWith(fontSize: 13))),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -418,10 +512,11 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Form(
|
||||
key: controller.messagesFormKey,
|
||||
@@ -429,24 +524,28 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
controller: controller.messageToDriver,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Type your message...'.tr,
|
||||
hintStyle: TextStyle(color: Colors.grey[500], fontSize: 13),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.only(bottom: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
CircleAvatar(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (controller.messagesFormKey.currentState!.validate()) {
|
||||
_sendMessage(controller, controller.messageToDriver.text);
|
||||
controller.messageToDriver.clear();
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.send_rounded, color: Colors.white, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
if (controller.messagesFormKey.currentState!.validate()) {
|
||||
_sendMessage(controller, controller.messageToDriver.text);
|
||||
controller.messageToDriver.clear();
|
||||
Get.back();
|
||||
}
|
||||
},
|
||||
child: CircleAvatar(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
radius: 20,
|
||||
child:
|
||||
const Icon(Icons.send_rounded, color: Colors.white, size: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -455,10 +554,10 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
|
||||
void _sendMessage(MapPassengerController controller, String text) {
|
||||
NotificationService.sendNotification(
|
||||
category: 'message From passenger',
|
||||
category: 'MSG_FROM_PASSENGER',
|
||||
target: controller.driverToken.toString(),
|
||||
title: 'Message From passenger'.tr,
|
||||
body: text,
|
||||
title: text.tr,
|
||||
body: text.tr,
|
||||
isTopic: false,
|
||||
tone: 'ding',
|
||||
driverList: [],
|
||||
@@ -467,7 +566,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// مؤشرات الانتظار والوقت (نفس المنطق مع تحسين بسيط في التصميم)
|
||||
// مؤشرات الانتظار والوقت (مضغوطة)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class DriverArrivePassengerAndWaitMinute extends StatelessWidget {
|
||||
@@ -481,24 +580,27 @@ class DriverArrivePassengerAndWaitMinute extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Driver is waiting'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text('Waiting...'.tr,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
color: Colors.orange)),
|
||||
Text(
|
||||
controller.stringRemainingTimeDriverWaitPassenger5Minute,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: AppColor.redColor),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange,
|
||||
fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const SizedBox(height: 4),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor: Colors.grey[200],
|
||||
color: controller.remainingTimeDriverWaitPassenger5Minute < 60
|
||||
? AppColor.redColor
|
||||
: AppColor.greenColor,
|
||||
minHeight: 8,
|
||||
backgroundColor: Colors.orange.withOpacity(0.2),
|
||||
color: Colors.orange,
|
||||
minHeight: 4,
|
||||
value:
|
||||
controller.progressTimerDriverWaitPassenger5Minute.toDouble(),
|
||||
),
|
||||
@@ -520,25 +622,13 @@ class TimeDriverToPassenger extends StatelessWidget {
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Driver arriving in'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
controller.stringRemainingTimeToPassenger,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: AppColor.primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// شريط التقدم فقط لأن الوقت والمسافة موجودان بالأعلى
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor: Colors.grey[200],
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
color: AppColor.primaryColor,
|
||||
minHeight: 8,
|
||||
minHeight: 4,
|
||||
value: controller.progressTimerToPassengerFromDriverAfterApplied
|
||||
.toDouble()
|
||||
.clamp(0.0, 1.0),
|
||||
|
||||
@@ -5,97 +5,156 @@ import 'package:Intaleq/constant/style.dart';
|
||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
|
||||
// دالة لإظهار الشيت
|
||||
void showCancelRideBottomSheet() {
|
||||
Get.bottomSheet(
|
||||
cancelRidePage(),
|
||||
const CancelRidePageWidget(),
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
);
|
||||
}
|
||||
|
||||
GetBuilder<MapPassengerController> cancelRidePage() {
|
||||
Get.put(MapPassengerController());
|
||||
// الويدجت مفصولة لترتيب الكود
|
||||
class CancelRidePageWidget extends StatelessWidget {
|
||||
const CancelRidePageWidget({Key? key}) : super(key: key);
|
||||
|
||||
final List<String> reasons = [
|
||||
"I don't need a ride anymore".tr,
|
||||
"I was just trying the application".tr,
|
||||
"No driver accepted my request".tr,
|
||||
"I added the wrong pick-up/drop-off location".tr,
|
||||
"I don't have a reason".tr,
|
||||
"Other".tr,
|
||||
];
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// تأكد من وجود الكنترولر
|
||||
final controller = Get.find<MapPassengerController>();
|
||||
|
||||
return GetBuilder<MapPassengerController>(
|
||||
builder: (controller) => controller.isCancelRidePageShown
|
||||
? Container(
|
||||
height: Get.height * 0.6,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
offset: const Offset(0, 8),
|
||||
blurRadius: 16,
|
||||
final List<String> reasons = [
|
||||
"Changed my mind".tr,
|
||||
"Found another transport".tr,
|
||||
"Driver is taking too long".tr,
|
||||
"Driver asked me to cancel".tr,
|
||||
"Wrong pickup location".tr,
|
||||
"Other".tr,
|
||||
];
|
||||
|
||||
return Container(
|
||||
height: Get.height * 0.7, // ارتفاع مناسب
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(25)),
|
||||
),
|
||||
child: GetBuilder<MapPassengerController>(
|
||||
builder: (controller) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// مؤشر السحب
|
||||
Center(
|
||||
child: Container(
|
||||
width: 50,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Can we know why you want to cancel Ride ?'.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: reasons.length,
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
const SizedBox(height: 20),
|
||||
|
||||
Text(
|
||||
'Why do you want to cancel?'.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: reasons.length,
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
bool isSelected = controller.selectedReasonIndex == index;
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(
|
||||
reasons[index],
|
||||
style: AppStyle.title.copyWith(fontSize: 16),
|
||||
),
|
||||
leading: Radio(
|
||||
value: index,
|
||||
groupValue: controller.selectedReason,
|
||||
onChanged: (int? value) {
|
||||
controller.selectReason(value!, reasons[index]);
|
||||
},
|
||||
activeColor: AppColor.primaryColor,
|
||||
style: TextStyle(
|
||||
fontWeight: isSelected
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color: isSelected
|
||||
? AppColor.primaryColor
|
||||
: Colors.black87,
|
||||
fontSize: 15),
|
||||
),
|
||||
trailing: isSelected
|
||||
? Icon(Icons.radio_button_checked,
|
||||
color: AppColor.primaryColor)
|
||||
: Icon(Icons.radio_button_off, color: Colors.grey),
|
||||
onTap: () {
|
||||
controller.selectReason(index, reasons[index]);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
MyElevatedButton(
|
||||
title: 'Cancel Ride'.tr,
|
||||
onPressed: () {
|
||||
if (controller.selectedReason == -1) {
|
||||
Get.snackbar(
|
||||
'You Should be select reason.'.tr,
|
||||
'',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: AppColor.redColor,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} else {
|
||||
controller.cancelRide();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// إظهار حقل النص فقط عند اختيار "أخرى"
|
||||
if (isSelected && reasons[index] == "Other".tr)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 10, left: 10, right: 10),
|
||||
child: TextField(
|
||||
controller: controller.otherReasonController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Please write the reason...".tr,
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100],
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 15, vertical: 12),
|
||||
),
|
||||
maxLines: 2,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
);
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// زر التأكيد
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.redColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 0,
|
||||
),
|
||||
onPressed: () => controller.cancelRide(),
|
||||
child: Text(
|
||||
'Confirm Cancellation'.tr,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// زر التراجع
|
||||
Center(
|
||||
child: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text("Don't Cancel".tr,
|
||||
style: TextStyle(color: Colors.grey[600])),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../widgets/mydialoug.dart';
|
||||
|
||||
// --- CarType class (unchanged) ---
|
||||
// --- CarType class (Unchanged) ---
|
||||
class CarType {
|
||||
final String carType;
|
||||
final String carDetail;
|
||||
@@ -27,21 +27,20 @@ class CarType {
|
||||
{required this.carType, required this.carDetail, required this.image});
|
||||
}
|
||||
|
||||
// --- List of Car Types (unchanged) ---
|
||||
// --- List of Car Types (Unchanged) ---
|
||||
List<CarType> carTypes = [
|
||||
CarType(
|
||||
carType: 'Speed',
|
||||
carType: 'Fixed Price',
|
||||
carDetail: 'Closest & Cheapest'.tr,
|
||||
image: 'assets/images/carspeed.png'), // First choice
|
||||
image: 'assets/images/carspeed.png'),
|
||||
CarType(
|
||||
carType: 'Comfort',
|
||||
carDetail: 'Comfort choice'.tr,
|
||||
image: 'assets/images/blob.png'), // Second choice
|
||||
image: 'assets/images/blob.png'),
|
||||
CarType(
|
||||
carType: 'Electric',
|
||||
carDetail: 'Quiet & Eco-Friendly'.tr,
|
||||
image:
|
||||
'assets/images/electric.png'), // Third choice - NOTE: Use your actual image path
|
||||
image: 'assets/images/electric.png'),
|
||||
CarType(
|
||||
carType: 'Lady',
|
||||
carDetail: 'Lady Captain for girls'.tr,
|
||||
@@ -62,8 +61,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
final textToSpeechController = Get.put(TextToSpeechController());
|
||||
|
||||
void _prepareCarTypes(MapPassengerController controller) {
|
||||
// This logic remains the same
|
||||
if (controller.distance > 33) {
|
||||
if (controller.distance > 23) {
|
||||
if (!carTypes.any((car) => car.carType == 'Rayeh Gai')) {
|
||||
carTypes.add(CarType(
|
||||
carType: 'Rayeh Gai',
|
||||
@@ -83,110 +81,403 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
if (!(controller.isBottomSheetShown) && controller.rideConfirm == false) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// Added a BackdropFilter for a modern glassmorphism effect
|
||||
|
||||
// Main Bottom Sheet Design
|
||||
return Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30),
|
||||
),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor.withOpacity(0.9),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30),
|
||||
),
|
||||
border: Border.all(color: AppColor.writeColor.withOpacity(0.1)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Added a small handle for visual cue
|
||||
Container(
|
||||
width: 40,
|
||||
height: 5,
|
||||
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.writeColor.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
_buildHeader(controller),
|
||||
_buildNegativeBalanceWarning(controller),
|
||||
SizedBox(
|
||||
height: 140, // Increased height for better spacing
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 12),
|
||||
itemCount: carTypes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final carType = carTypes[index];
|
||||
final isSelected = controller.selectedIndex == index;
|
||||
return _buildHorizontalCarCard(
|
||||
context, controller, carType, isSelected, index);
|
||||
},
|
||||
),
|
||||
),
|
||||
_buildPromoButton(context, controller),
|
||||
const SizedBox(height: 8), // Added padding at the bottom
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor
|
||||
.secondaryColor, // Solid background for better performance
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 5,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Drag Handle
|
||||
Center(
|
||||
child: Container(
|
||||
width: 50,
|
||||
height: 5,
|
||||
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Header (Title + Trip Info)
|
||||
_buildModernHeader(controller),
|
||||
|
||||
// Warning Message (if any)
|
||||
_buildNegativeBalanceWarning(controller),
|
||||
|
||||
// Car List
|
||||
SizedBox(
|
||||
height: 165, // Fixed height for consistency
|
||||
child: ListView.separated(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
itemCount: carTypes.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(width: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final carType = carTypes[index];
|
||||
final isSelected = controller.selectedIndex == index;
|
||||
return _buildVerticalCarCard(
|
||||
context, controller, carType, isSelected, index);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Promo Code Button
|
||||
_buildPromoButton(context, controller),
|
||||
|
||||
// Safe Area spacing
|
||||
SizedBox(height: MediaQuery.of(context).padding.bottom + 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// --- All other methods are here, with updated designs ---
|
||||
// --- UI Components ---
|
||||
|
||||
Widget _buildModernHeader(MapPassengerController controller) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 5),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Choose your ride'.tr,
|
||||
style: AppStyle.headTitle.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0.5),
|
||||
),
|
||||
),
|
||||
|
||||
// Trip Info Pill
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: AppColor.primaryColor.withOpacity(0.2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.directions_car_filled_outlined,
|
||||
size: 16, color: AppColor.primaryColor),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'${controller.distance.toStringAsFixed(1)} ${'KM'.tr}',
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.primaryColor),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
height: 12,
|
||||
width: 1,
|
||||
color: Colors.grey.shade400),
|
||||
Icon(Icons.access_time_filled_rounded,
|
||||
size: 16, color: AppColor.primaryColor),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
controller.hours > 0
|
||||
? '${controller.hours}h ${controller.minutes}m'
|
||||
: '${controller.minutes} min',
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVerticalCarCard(
|
||||
BuildContext context,
|
||||
MapPassengerController controller,
|
||||
CarType carType,
|
||||
bool isSelected,
|
||||
int index) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
controller.selectCarFromList(index);
|
||||
_showCarDetailsDialog(
|
||||
context, controller, carType, textToSpeechController);
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
width: 110,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? AppColor.primaryColor.withOpacity(0.05)
|
||||
: Colors.white,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? AppColor.primaryColor
|
||||
: Colors.grey.withOpacity(0.2),
|
||||
width: isSelected ? 2.0 : 1.0,
|
||||
),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColor.primaryColor.withOpacity(0.2),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
]
|
||||
: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Image with subtle scaling if selected
|
||||
AnimatedScale(
|
||||
scale: isSelected ? 1.1 : 1.0,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: Image.asset(
|
||||
carType.image,
|
||||
height: 50,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Car Type Text
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
carType.carType.tr,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.w800 : FontWeight.w600,
|
||||
fontSize: 14,
|
||||
color:
|
||||
isSelected ? AppColor.primaryColor : Colors.black87,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// Price Tag
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? AppColor.primaryColor
|
||||
: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: FittedBox(
|
||||
child: Text(
|
||||
'${_getPassengerPriceText(carType, controller)} ${'SYP'.tr}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isSelected ? Colors.white : Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Checkmark Badge for Selected Item
|
||||
if (isSelected)
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColor.primaryColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.check, size: 12, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPromoButton(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
if (controller.promoTaken) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (controller.promoTaken) return const SizedBox.shrink();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||||
child: GestureDetector(
|
||||
onTap: () => _showPromoCodeDialog(context, controller),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: AppColor.primaryColor.withOpacity(0.5),
|
||||
width: 1,
|
||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 5),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _showPromoCodeDialog(context, controller),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor.withOpacity(0.1),
|
||||
shape: BoxShape.circle),
|
||||
child: Icon(Icons.confirmation_number_outlined,
|
||||
color: AppColor.primaryColor, size: 20),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Promo Code'.tr,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
fontSize: 14, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
'Have a promo code?'.tr,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_ios_rounded,
|
||||
size: 16, color: Colors.grey.shade400)
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.local_offer_outlined,
|
||||
color: AppColor.primaryColor, size: 22),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'Have a promo code?'.tr,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 16,
|
||||
color: AppColor.primaryColor,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNegativeBalanceWarning(MapPassengerController controller) {
|
||||
final passengerWallet =
|
||||
double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ?? 0.0;
|
||||
if (passengerWallet < 0.0) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.redColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.redColor.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline_rounded,
|
||||
color: AppColor.redColor, size: 24),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${'You have a negative balance of'.tr} ${passengerWallet.toStringAsFixed(2)} ${'SYP'.tr}.',
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: AppColor.redColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// --- Logic Helpers (Copied from your previous code to ensure functionality) ---
|
||||
|
||||
String _getPassengerPriceText(
|
||||
CarType carType, MapPassengerController mapPassengerController) {
|
||||
double rawPrice;
|
||||
switch (carType.carType) {
|
||||
case 'Comfort':
|
||||
rawPrice = mapPassengerController.totalPassengerComfort;
|
||||
break;
|
||||
case 'Fixed Price':
|
||||
rawPrice = mapPassengerController.totalPassengerSpeed;
|
||||
break;
|
||||
case 'Electric':
|
||||
rawPrice = mapPassengerController.totalPassengerElectric;
|
||||
break;
|
||||
case 'Awfar Car':
|
||||
rawPrice = mapPassengerController.totalPassengerBalash;
|
||||
break;
|
||||
case 'Scooter':
|
||||
case 'Pink Bike':
|
||||
rawPrice = mapPassengerController.totalPassengerScooter;
|
||||
break;
|
||||
case 'Van':
|
||||
rawPrice = mapPassengerController.totalPassengerVan;
|
||||
break;
|
||||
case 'Lady':
|
||||
rawPrice = mapPassengerController.totalPassengerLady;
|
||||
break;
|
||||
case 'Rayeh Gai':
|
||||
rawPrice = mapPassengerController.totalPassengerRayehGai;
|
||||
break;
|
||||
default:
|
||||
return '...';
|
||||
}
|
||||
final int roundedPrice = rawPrice.round();
|
||||
final formatter = NumberFormat.decimalPattern();
|
||||
return formatter.format(roundedPrice);
|
||||
}
|
||||
|
||||
// --- Dialogs (Styled consistently) ---
|
||||
|
||||
void _showPromoCodeDialog(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
Get.dialog(
|
||||
@@ -194,6 +485,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),
|
||||
backgroundColor: AppColor.secondaryColor,
|
||||
elevation: 10,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
@@ -202,17 +494,20 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Icon(Icons.local_activity_outlined,
|
||||
size: 40, color: AppColor.primaryColor),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Apply Promo Code'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.headTitle.copyWith(fontSize: 22),
|
||||
style: AppStyle.headTitle.copyWith(fontSize: 20),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Enter your code below to apply the discount.'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(color: AppColor.writeColor.withOpacity(0.7)),
|
||||
.copyWith(color: Colors.grey.shade600, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
MyTextForm(
|
||||
@@ -225,15 +520,10 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
child: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColor.writeColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
side: BorderSide(
|
||||
color: AppColor.writeColor.withOpacity(0.3)),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.grey,
|
||||
),
|
||||
child: Text('Cancel'.tr),
|
||||
),
|
||||
@@ -262,211 +552,6 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHorizontalCarCard(
|
||||
BuildContext context,
|
||||
MapPassengerController controller,
|
||||
CarType carType,
|
||||
bool isSelected,
|
||||
int index) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
controller.selectCarFromList(index);
|
||||
_showCarDetailsDialog(
|
||||
context, controller, carType, textToSpeechController);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: 120, // Increased width
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
padding: const EdgeInsets.all(8), // Added padding
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: isSelected
|
||||
? [
|
||||
AppColor.primaryColor.withOpacity(0.3),
|
||||
AppColor.primaryColor.withOpacity(0.1)
|
||||
]
|
||||
: [
|
||||
AppColor.writeColor.withOpacity(0.05),
|
||||
AppColor.writeColor.withOpacity(0.1)
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20), // More rounded corners
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? AppColor.primaryColor
|
||||
: AppColor.writeColor.withOpacity(0.2),
|
||||
width: isSelected ? 2.5 : 1.0,
|
||||
),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColor.primaryColor.withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 1,
|
||||
)
|
||||
]
|
||||
: [],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround, // Better alignment
|
||||
children: [
|
||||
Image.asset(carType.image, height: 55), // Slightly larger image
|
||||
Text(
|
||||
carType.carType.tr,
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(fontWeight: FontWeight.bold, fontSize: 15),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
_buildPriceDisplay(controller, carType),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(MapPassengerController controller) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Choose your ride'.tr,
|
||||
style: AppStyle.headTitle.copyWith(fontSize: 24)),
|
||||
const SizedBox(height: 8),
|
||||
// Added icons for better visual representation
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.map_outlined,
|
||||
color: AppColor.primaryColor, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${controller.distance.toStringAsFixed(1)} ${'KM'.tr}',
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: AppColor.primaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Icon(Icons.timer_outlined,
|
||||
color: AppColor.primaryColor, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
controller.hours > 0
|
||||
? '${controller.hours}h ${controller.minutes}m'
|
||||
: '${controller.minutes} min',
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: AppColor.primaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNegativeBalanceWarning(MapPassengerController controller) {
|
||||
final passengerWallet =
|
||||
double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ?? 0.0;
|
||||
if (passengerWallet < 0.0) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.redColor.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.white, size: 24),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${'You have a negative balance of'.tr} ${passengerWallet.toStringAsFixed(2)} ${'SYP'.tr}.',
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: Colors.white, fontWeight: FontWeight.w600))),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget _buildPriceDisplay(
|
||||
MapPassengerController mapPassengerController, CarType carType) {
|
||||
return Text(
|
||||
'${_getPassengerPriceText(carType, mapPassengerController)} ${'SYP'.tr}',
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
fontSize: 15, // Increased font size
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.primaryColor));
|
||||
}
|
||||
|
||||
// --- LOGIC METHODS (UNCHANGED) ---
|
||||
|
||||
// 1. قم بإضافة هذا السطر في أعلى الملف
|
||||
|
||||
String _getPassengerPriceText(
|
||||
CarType carType, MapPassengerController mapPassengerController) {
|
||||
// الخطوة 1: احصل على السعر كـ double أولاً
|
||||
double rawPrice;
|
||||
switch (carType.carType) {
|
||||
case 'Comfort':
|
||||
rawPrice = mapPassengerController.totalPassengerComfort;
|
||||
break;
|
||||
case 'Speed':
|
||||
rawPrice = mapPassengerController.totalPassengerSpeed;
|
||||
break;
|
||||
case 'Electric':
|
||||
rawPrice = mapPassengerController.totalPassengerElectric;
|
||||
break;
|
||||
case 'Awfar Car':
|
||||
rawPrice = mapPassengerController.totalPassengerBalash;
|
||||
break;
|
||||
case 'Scooter':
|
||||
case 'Pink Bike': // دمج الحالات المتشابهة
|
||||
rawPrice = mapPassengerController.totalPassengerScooter;
|
||||
break;
|
||||
case 'Van':
|
||||
rawPrice = mapPassengerController.totalPassengerVan;
|
||||
break;
|
||||
case 'Lady':
|
||||
rawPrice = mapPassengerController.totalPassengerLady;
|
||||
break;
|
||||
case 'Rayeh Gai':
|
||||
rawPrice = mapPassengerController.totalPassengerRayehGai;
|
||||
break;
|
||||
default:
|
||||
return '...'; // إذا كان نوع السيارة غير معروف
|
||||
}
|
||||
|
||||
// الخطوة 2: قم بإزالة الكسور العشرية
|
||||
// .round() ستحول 65000.00 إلى 65000
|
||||
final int roundedPrice = rawPrice.round();
|
||||
|
||||
// الخطوة 3: أنشئ "مُنسّق" ليضيف فواصل الآلاف
|
||||
// NumberFormat.decimalPattern() يستخدم إعدادات اللغة الافتراضية للجهاز
|
||||
// لوضع الفاصلة (,) أو النقطة (.) حسب الدولة
|
||||
final formatter = NumberFormat.decimalPattern();
|
||||
|
||||
// الخطوة 4: قم بتنسيق الرقم الصحيح
|
||||
// سيحول 65000 إلى "65,000"
|
||||
return formatter.format(roundedPrice);
|
||||
}
|
||||
|
||||
void _showCarDetailsDialog(
|
||||
BuildContext context,
|
||||
MapPassengerController mapPassengerController,
|
||||
@@ -475,74 +560,75 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(24.0)),
|
||||
backgroundColor:
|
||||
Colors.transparent, // Make dialog background transparent
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(28.0)),
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
// Main content container
|
||||
Container(
|
||||
margin:
|
||||
const EdgeInsets.only(top: 50), // Make space for the image
|
||||
padding: const EdgeInsets.fromLTRB(20, 70, 20, 20),
|
||||
margin: const EdgeInsets.only(top: 60),
|
||||
padding: const EdgeInsets.fromLTRB(24, 70, 24, 24),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderRadius: BorderRadius.circular(28.0),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
carType.carType.tr,
|
||||
style: AppStyle.headTitle.copyWith(fontSize: 24),
|
||||
style: AppStyle.headTitle.copyWith(fontSize: 22),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
onPressed: () => textToSpeechController.speakText(
|
||||
InkWell(
|
||||
onTap: () => textToSpeechController.speakText(
|
||||
_getCarDescription(
|
||||
mapPassengerController, carType)),
|
||||
icon: Icon(Icons.volume_up_outlined,
|
||||
color: AppColor.primaryColor, size: 26),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Icon(Icons.volume_up_rounded,
|
||||
color: AppColor.primaryColor, size: 24),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_getCarDescription(mapPassengerController, carType),
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: AppColor.writeColor.withOpacity(0.7),
|
||||
fontSize: 16,
|
||||
height: 1.5,
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(16)),
|
||||
child: Text(
|
||||
_getCarDescription(mapPassengerController, carType),
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: Colors.black87,
|
||||
fontSize: 15,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
AppColor.writeColor.withOpacity(0.8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Text('Cancel'.tr),
|
||||
child: Text('Back'.tr,
|
||||
style: TextStyle(color: Colors.grey.shade600)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: MyElevatedButton(
|
||||
kolor: AppColor.greenColor,
|
||||
title: 'Next'.tr,
|
||||
title: 'Select This Ride'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
_handleCarSelection(
|
||||
@@ -555,10 +641,12 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Positioned car image
|
||||
Positioned(
|
||||
top: -10,
|
||||
child: Image.asset(carType.image, height: 120),
|
||||
top: 0,
|
||||
child: Hero(
|
||||
tag: 'car_${carType.carType}',
|
||||
child: Image.asset(carType.image, height: 130),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -567,6 +655,8 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// --- Logic Helpers (Keep unchanged) ---
|
||||
|
||||
String _getCarDescription(
|
||||
MapPassengerController mapPassengerController, CarType carType) {
|
||||
switch (carType.carType) {
|
||||
@@ -578,7 +668,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
.tr
|
||||
: 'Best choice for comfort car and flexible route and stops point'
|
||||
.tr;
|
||||
case 'Speed':
|
||||
case 'Fixed Price':
|
||||
return 'This trip goes directly from your starting point to your destination for a fixed price. The driver must follow the planned route'
|
||||
.tr;
|
||||
case 'Electric':
|
||||
@@ -632,7 +722,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||
_buildRayehGaiOption(mapPassengerController, 'Awfar Car',
|
||||
mapPassengerController.totalPassengerRayehGaiBalash),
|
||||
_buildRayehGaiOption(mapPassengerController, 'Speed',
|
||||
_buildRayehGaiOption(mapPassengerController, 'Fixed Price',
|
||||
mapPassengerController.totalPassengerRayehGai),
|
||||
_buildRayehGaiOption(mapPassengerController, 'Comfort',
|
||||
mapPassengerController.totalPassengerRayehGaiComfort)
|
||||
@@ -661,7 +751,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
switch (carType.carType) {
|
||||
case 'Comfort':
|
||||
return mapPassengerController.totalPassengerComfort;
|
||||
case 'Speed':
|
||||
case 'Fixed Price':
|
||||
return mapPassengerController.totalPassengerSpeed;
|
||||
case 'Electric':
|
||||
return mapPassengerController.totalPassengerElectric;
|
||||
|
||||
@@ -7,53 +7,81 @@ import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
|
||||
// ---------------------------------------------------
|
||||
// -- Widget for Start Point Search --
|
||||
// -- Widget for Start Point Search (Updated) --
|
||||
// ---------------------------------------------------
|
||||
|
||||
GetBuilder<MapPassengerController> formSearchPlacesStart() {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
id: 'start_point_form', // إضافة معرف لتحديث هذا الجزء فقط عند الحاجة
|
||||
builder: (controller) => Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: TextFormField(
|
||||
controller: controller.placeStartController,
|
||||
onChanged: (value) {
|
||||
if (controller.placeStartController.text.length > 2) {
|
||||
controller.getPlacesStart();
|
||||
} else if (controller.placeStartController.text.isEmpty) {
|
||||
controller.clearPlacesStart();
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search for a starting point'.tr,
|
||||
hintStyle: AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
||||
prefixIcon:
|
||||
const Icon(Icons.search, color: AppColor.primaryColor),
|
||||
suffixIcon: controller.placeStartController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
||||
onPressed: () {
|
||||
controller.placeStartController.clear();
|
||||
controller.clearPlacesStart();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide.none,
|
||||
child: Row(
|
||||
children: [
|
||||
// --- حقل البحث النصي ---
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller.placeStartController,
|
||||
onChanged: (value) {
|
||||
if (controller.placeStartController.text.length > 2) {
|
||||
controller.getPlacesStart();
|
||||
} else if (controller.placeStartController.text.isEmpty) {
|
||||
controller.clearPlacesStart();
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search for a starting point'.tr,
|
||||
hintStyle:
|
||||
AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
||||
prefixIcon:
|
||||
const Icon(Icons.search, color: AppColor.primaryColor),
|
||||
suffixIcon: controller.placeStartController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
||||
onPressed: () {
|
||||
controller.placeStartController.clear();
|
||||
controller.clearPlacesStart();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0, vertical: 10.0),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide(color: AppColor.primaryColor),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[50],
|
||||
),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide(color: AppColor.primaryColor),
|
||||
|
||||
const SizedBox(width: 8.0),
|
||||
|
||||
// --- أيقونة اختيار الموقع من الخريطة (الجزء المضاف) ---
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// هذا السطر مهم جداً: نخبر الكونترولر أننا نحدد نقطة البداية الآن
|
||||
controller.passengerStartLocationFromMap = true;
|
||||
|
||||
// إخفاء القائمة السفلية وفتح مؤشر الخريطة (Picker)
|
||||
controller.changeMainBottomMenuMap();
|
||||
controller.changePickerShown();
|
||||
},
|
||||
icon: Icon(Icons.location_on_outlined,
|
||||
color: AppColor.accentColor, size: 30),
|
||||
tooltip: 'Pick start point on map'.tr,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[50],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- قائمة نتائج البحث ---
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: controller.placesStart.isNotEmpty ? 300 : 0,
|
||||
@@ -84,11 +112,19 @@ GetBuilder<MapPassengerController> formSearchPlacesStart() {
|
||||
var latitude = res['latitude'];
|
||||
var longitude = res['longitude'];
|
||||
if (latitude != null && longitude != null) {
|
||||
// تحديث موقع الراكب (نقطة الانطلاق) بناءً على الاختيار
|
||||
controller.passengerLocation =
|
||||
LatLng(double.parse(latitude), double.parse(longitude));
|
||||
|
||||
// تحديث النص في الحقل
|
||||
controller.placeStartController.text = title;
|
||||
|
||||
// مسح النتائج
|
||||
controller.clearPlacesStart();
|
||||
// You might want to update the camera position on the map here
|
||||
|
||||
// إغلاق القائمة والعودة للخريطة لرؤية النتيجة (اختياري حسب منطق تطبيقك)
|
||||
controller.changeMainBottomMenuMap();
|
||||
|
||||
controller.update();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -147,7 +147,7 @@ class GoogleMapPassengerWidget extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
|
||||
myLocationEnabled: true,
|
||||
myLocationEnabled: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/vip_waitting_page.dart';
|
||||
import '../../../env/env.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../auth/otp_page.dart';
|
||||
|
||||
// --- الدالة الرئيسية بالتصميم الجديد ---
|
||||
GetBuilder<MapPassengerController> leftMainMenuIcons() {
|
||||
@@ -35,9 +36,9 @@ GetBuilder<MapPassengerController> leftMainMenuIcons() {
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor.withOpacity(0.8), // لون شبه شفاف
|
||||
color: AppColor.secondaryColor.withOpacity(0.4), // لون شبه شفاف
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
border: Border.all(color: AppColor.writeColor.withOpacity(0.2)),
|
||||
border: Border.all(color: AppColor.secondaryColor),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -49,12 +50,12 @@ GetBuilder<MapPassengerController> leftMainMenuIcons() {
|
||||
tooltip: 'Toggle Map Type',
|
||||
onPressed: () => controller.changeMapType(),
|
||||
),
|
||||
_buildVerticalDivider(),
|
||||
_buildMapActionButton(
|
||||
icon: Icons.traffic_outlined,
|
||||
tooltip: 'Toggle Traffic',
|
||||
onPressed: () => controller.changeMapTraffic(),
|
||||
),
|
||||
// _buildVerticalDivider(),
|
||||
// _buildMapActionButton(
|
||||
// icon: Icons.traffic_outlined,
|
||||
// tooltip: 'Toggle Traffic',
|
||||
// onPressed: () => controller.changeMapTraffic(),
|
||||
// ),
|
||||
_buildVerticalDivider(),
|
||||
_buildMapActionButton(
|
||||
icon: Icons.my_location_rounded,
|
||||
@@ -129,22 +130,9 @@ class TestPage extends StatelessWidget {
|
||||
body: Center(
|
||||
child: TextButton(
|
||||
onPressed: () async {
|
||||
// var token = (box.read(BoxName.tokenFCM).toString());
|
||||
// Log.print(
|
||||
// 'box.read(BoxName.tokenFCM).toString(): ${box.read(BoxName.tokenFCM).toString()}');
|
||||
// // 'e-EE5Z5Fn0x5s6EYbtgT6f:APA91bHBTxkbdljuvDF0iPhso58r7fCwGh-WcYh3CYfUJEShUKFcQf496Xc5E6LHqRFKfOQBxYrWSdLO8d9gLbL-IdgyDuZ7jNUjzvrcV_YmagDtgz7-UNw';
|
||||
// // 'fdN1o8akwURHj47wvShC4T:APA91bFm-mFfFjdCbHsDReN0MzPE1hiaHKtPJnzayMec6LiInjzk6YCX41SeF0T1FE7Z6d4Hjy1AkZhLIeebSgX4RrodzwSwZSH0kboTQEfqkrjrk4xw9aM';
|
||||
// NotificationService.sendNotification(
|
||||
// target:
|
||||
// 'eznj5vRWRnqwKNtKJBaYNg:APA91bHhJ2DJ1KQa3KRx6wQtX8BkFHq6I_-dXGxT16p6pnV5AwI0bWOeiTJOI35VfTBaK4YSCKmAB4SsRnpARK0MTJ96xtpPmwAKfkvsZFga8OoGMeb3PmA',
|
||||
// title: 'Order',
|
||||
// body: 'endNameAddress',
|
||||
// isTopic: false,
|
||||
// tone: 'tone1',
|
||||
// category: 'Order', // استخدام الفئة الثابتة
|
||||
// driverList: []);
|
||||
// RideState.driverApplied;
|
||||
// Get.find<MapPassengerController>().Ride
|
||||
// print(box.read(BoxName.lowEndMode));
|
||||
// box.read(BoxName.lowEndMode)
|
||||
Get.to(PhoneNumberScreen());
|
||||
},
|
||||
child: Text(
|
||||
"Text Button",
|
||||
|
||||
@@ -533,12 +533,12 @@ class MainBottomMenuMap extends StatelessWidget {
|
||||
|
||||
// else
|
||||
|
||||
Text(
|
||||
controller.nearestCar != null
|
||||
? 'Nearest Car: ${controller.nearestDistance.toStringAsFixed(0)} m'
|
||||
: 'No cars nearby'.tr,
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
// Text(
|
||||
// controller.nearestCar != null
|
||||
// ? 'Nearest Car: ${controller.nearestDistance.toStringAsFixed(0)} m'
|
||||
// : 'No cars nearby'.tr,
|
||||
// style: TextStyle(color: Colors.grey.shade600),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,297 +1,10 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:Intaleq/constant/box_name.dart';
|
||||
// import 'package:Intaleq/controller/profile/profile_controller.dart';
|
||||
// import 'package:Intaleq/main.dart';
|
||||
// import 'package:Intaleq/views/home/profile/complaint_page.dart';
|
||||
|
||||
// import '../../../constant/colors.dart';
|
||||
// import '../../../constant/links.dart';
|
||||
// import '../../../constant/style.dart';
|
||||
// import '../../../controller/functions/audio_record1.dart';
|
||||
// import '../../../controller/functions/launch.dart';
|
||||
// import '../../../controller/functions/toast.dart';
|
||||
// import '../../../controller/home/map_passenger_controller.dart';
|
||||
|
||||
// // --- الويدجت الرئيسية بالتصميم الجديد ---
|
||||
// class RideBeginPassenger extends StatelessWidget {
|
||||
// const RideBeginPassenger({super.key});
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// // --- نفس منطق استدعاء الكنترولرز ---
|
||||
// final ProfileController profileController = Get.put(ProfileController());
|
||||
// final AudioRecorderController audioController =
|
||||
// Get.put(AudioRecorderController());
|
||||
|
||||
// return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
// // --- نفس شرط الإظهار الخاص بك ---
|
||||
// if (controller.statusRide != 'Begin') {
|
||||
// return const SizedBox.shrink();
|
||||
// }
|
||||
|
||||
// return Positioned(
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// bottom: 0,
|
||||
// child: Container(
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColor.secondaryColor,
|
||||
// borderRadius: const BorderRadius.only(
|
||||
// topLeft: Radius.circular(24),
|
||||
// topRight: Radius.circular(24),
|
||||
// ),
|
||||
// boxShadow: [
|
||||
// BoxShadow(
|
||||
// color: Colors.black.withOpacity(0.2),
|
||||
// blurRadius: 20,
|
||||
// offset: const Offset(0, -5),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// // مقبض السحب (Handle)
|
||||
// Container(
|
||||
// width: 40,
|
||||
// height: 5,
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColor.writeColor.withOpacity(0.3),
|
||||
// borderRadius: BorderRadius.circular(12),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 12),
|
||||
|
||||
// // --- 1. قسم معلومات السائق ---
|
||||
// _buildDriverInfo(controller),
|
||||
// const Divider(height: 24, thickness: 0.5),
|
||||
|
||||
// // --- 2. قسم تقدم الرحلة ---
|
||||
// _buildTripProgress(controller),
|
||||
// const SizedBox(height: 16),
|
||||
|
||||
// // --- 3. قسم الإجراءات والأمان ---
|
||||
// _buildActionButtons(
|
||||
// context, controller, profileController, audioController),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
// // --- ويدجت مساعدة لعرض معلومات السائق بشكل منظم ---
|
||||
// Widget _buildDriverInfo(MapPassengerController controller) {
|
||||
// return Row(
|
||||
// children: [
|
||||
// CircleAvatar(
|
||||
// radius: 28,
|
||||
// backgroundImage: NetworkImage(
|
||||
// '${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'),
|
||||
// ),
|
||||
// const SizedBox(width: 12),
|
||||
// Expanded(
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Text(controller.driverName,
|
||||
// style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
||||
// const SizedBox(height: 2),
|
||||
// Text(
|
||||
// '${controller.make} ${controller.model} • ${box.read(BoxName.carType)}',
|
||||
// style: AppStyle.subtitle
|
||||
// .copyWith(color: AppColor.writeColor.withOpacity(0.7)),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 12),
|
||||
// Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.end,
|
||||
// children: [
|
||||
// Container(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColor.writeColor.withOpacity(0.1),
|
||||
// borderRadius: BorderRadius.circular(6),
|
||||
// ),
|
||||
// child: Text(
|
||||
// controller.licensePlate,
|
||||
// style: AppStyle.subtitle
|
||||
// .copyWith(fontWeight: FontWeight.bold, letterSpacing: 1.5),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 4),
|
||||
// Row(
|
||||
// children: [
|
||||
// Text(controller.driverRate,
|
||||
// style: AppStyle.subtitle
|
||||
// .copyWith(fontWeight: FontWeight.bold)),
|
||||
// const SizedBox(width: 2),
|
||||
// const Icon(Icons.star_rounded,
|
||||
// color: AppColor.yellowColor, size: 16),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// )
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
|
||||
// // --- ويدجت مساعدة لعرض شريط التقدم ---
|
||||
// Widget _buildTripProgress(MapPassengerController controller) {
|
||||
// return Column(
|
||||
// children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Text('Time to Destination'.tr, style: AppStyle.subtitle),
|
||||
// Text(controller.stringRemainingTimeRideBegin,
|
||||
// style: AppStyle.subtitle.copyWith(
|
||||
// fontWeight: FontWeight.bold, color: AppColor.primaryColor)),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(height: 8),
|
||||
// ClipRRect(
|
||||
// borderRadius: BorderRadius.circular(10),
|
||||
// child: LinearProgressIndicator(
|
||||
// backgroundColor: AppColor.primaryColor.withOpacity(0.2),
|
||||
// color: controller.remainingTimeTimerRideBegin < 60
|
||||
// ? AppColor.redColor
|
||||
// : AppColor.greenColor,
|
||||
// minHeight: 10,
|
||||
// value: controller.progressTimerRideBegin.toDouble(),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
|
||||
// // --- ويدجت مساعدة لعرض أزرار الإجراءات ---
|
||||
// Widget _buildActionButtons(
|
||||
// BuildContext context,
|
||||
// MapPassengerController controller,
|
||||
// ProfileController profileController,
|
||||
// AudioRecorderController audioController) {
|
||||
// return Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
// children: [
|
||||
// _buildActionButton(
|
||||
// icon: Icons.sos_rounded,
|
||||
// label: 'SOS'.tr,
|
||||
// color: AppColor.redColor,
|
||||
// onTap: () async {
|
||||
// // --- نفس منطقك القديم ---
|
||||
// if (box.read(BoxName.sosPhonePassenger) == null) {
|
||||
// await profileController.updatField(
|
||||
// 'sosPhone', TextInputType.phone);
|
||||
// box.write(BoxName.sosPhonePassenger,
|
||||
// profileController.prfoileData['sosPhone']);
|
||||
// } else {
|
||||
// makePhoneCall('112');
|
||||
// }
|
||||
// }),
|
||||
// _buildActionButton(
|
||||
// icon: FontAwesome.whatsapp,
|
||||
// label: 'WhatsApp'.tr,
|
||||
// color: AppColor.greenColor,
|
||||
// onTap: () async {
|
||||
// // --- نفس منطقك القديم ---
|
||||
// if (box.read(BoxName.sosPhonePassenger) == null ||
|
||||
// box.read(BoxName.sosPhonePassenger) == 'sos') {
|
||||
// await profileController.updatField(
|
||||
// 'sosPhone', TextInputType.phone);
|
||||
// box.write(BoxName.sosPhonePassenger,
|
||||
// profileController.prfoileData['sosPhone']);
|
||||
// } else {
|
||||
// final phoneNumber =
|
||||
// box.read(BoxName.sosPhonePassenger).toString();
|
||||
|
||||
// final phone = controller.formatSyrianPhoneNumber(phoneNumber);
|
||||
// controller.sendWhatsapp(phone); //
|
||||
// }
|
||||
// }),
|
||||
// _buildActionButton(
|
||||
// icon: Icons.share_location_outlined, // أيقونة جديدة ومناسبة
|
||||
// label: 'Share'.tr, // اسم جديد وواضح
|
||||
// color: AppColor.blueColor,
|
||||
// onTap: () async {
|
||||
// // نفس الوظيفة السابقة التي كانت تحت اسم "Video Call"
|
||||
// await controller.getTokenForParent();
|
||||
// }),
|
||||
// _buildActionButton(
|
||||
// icon: audioController.isRecording
|
||||
// ? Icons.mic_off_rounded
|
||||
// : Icons.mic_none_rounded,
|
||||
// label: audioController.isRecording ? 'Stop'.tr : 'Record'.tr,
|
||||
// color: AppColor.primaryColor,
|
||||
// onTap: () async {
|
||||
// // --- نفس منطقك القديم ---
|
||||
// if (audioController.isRecording == false) {
|
||||
// await audioController.startRecording();
|
||||
// Toast.show(context, 'Start Record'.tr, AppColor.greenColor);
|
||||
// } else {
|
||||
// await audioController.stopRecording();
|
||||
// Toast.show(context, 'Record saved'.tr, AppColor.greenColor);
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// _buildActionButton(
|
||||
// icon: Icons.note_add_outlined,
|
||||
// label: 'Complaint'.tr,
|
||||
// color: AppColor.yellowColor,
|
||||
// onTap: () {
|
||||
// // --- نفس منطقك القديم ---
|
||||
// Get.to(() => ComplaintPage(), transition: Transition.downToUp);
|
||||
// }),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
|
||||
// // --- ويدجت مساعدة لبناء زر إجراء فردي ---
|
||||
// Widget _buildActionButton(
|
||||
// {required IconData icon,
|
||||
// required String label,
|
||||
// required Color color,
|
||||
// required VoidCallback onTap}) {
|
||||
// return InkWell(
|
||||
// onTap: onTap,
|
||||
// borderRadius: BorderRadius.circular(12),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(4.0),
|
||||
// child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
// children: [
|
||||
// Container(
|
||||
// padding: const EdgeInsets.all(12),
|
||||
// decoration: BoxDecoration(
|
||||
// color: color.withOpacity(0.1),
|
||||
// shape: BoxShape.circle,
|
||||
// ),
|
||||
// child: Icon(icon, color: color, size: 26),
|
||||
// ),
|
||||
// const SizedBox(height: 6),
|
||||
// Text(label, style: AppStyle.subtitle.copyWith(fontSize: 12)),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:Intaleq/constant/box_name.dart';
|
||||
import 'package:Intaleq/controller/profile/profile_controller.dart';
|
||||
import 'package:Intaleq/main.dart';
|
||||
import 'package:Intaleq/views/home/profile/complaint_page.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
// تأكد من المسارات
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
@@ -299,6 +12,9 @@ import '../../../controller/functions/audio_record1.dart';
|
||||
import '../../../controller/functions/launch.dart';
|
||||
import '../../../controller/functions/toast.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/profile/profile_controller.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../views/home/profile/complaint_page.dart';
|
||||
|
||||
class RideBeginPassenger extends StatelessWidget {
|
||||
const RideBeginPassenger({super.key});
|
||||
@@ -312,70 +28,69 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
return Obx(() {
|
||||
final controller = Get.find<MapPassengerController>();
|
||||
|
||||
// شرط الإظهار: تظهر فقط عندما تكون الرحلة جارية
|
||||
// شرط الإظهار
|
||||
final bool isVisible =
|
||||
controller.currentRideState.value == RideState.inProgress &&
|
||||
controller.isStartAppHasRide == false;
|
||||
;
|
||||
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeOutBack, // حركة أكثر سلاسة
|
||||
bottom: isVisible ? 0 : -Get.height * 0.6,
|
||||
curve: Curves.easeInOutCubic,
|
||||
// تم تقليل قيمة الإخفاء لأن الارتفاع الكلي للنافذة أصبح أصغر
|
||||
bottom: isVisible ? 0 : -300,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white, // خلفية بيضاء لنظافة التصميم
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30),
|
||||
topLeft: Radius.circular(25),
|
||||
topRight: Radius.circular(25),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 25,
|
||||
spreadRadius: 5,
|
||||
offset: const Offset(0, -5),
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 2,
|
||||
offset: const Offset(0, -3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 20),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// مقبض السحب
|
||||
Container(
|
||||
width: 50,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
// 1. مقبض السحب
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// الصف العلوي: معلومات السائق + السعر المثبت
|
||||
_buildDriverAndPriceSection(controller),
|
||||
// 2. هيدر المعلومات (سائق + سيارة + سعر)
|
||||
_buildCompactHeader(controller),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// الصف الأوسط: لوحة السيارة الواقعية + نوع السيارة
|
||||
_buildCarInfoSection(controller),
|
||||
// خط فاصل خفيف
|
||||
const Divider(
|
||||
height: 1, thickness: 0.5, color: Color(0xFFEEEEEE)),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// شريط التقدم والوقت
|
||||
_buildTripProgress(controller),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
const Divider(thickness: 1, color: Color(0xFFEEEEEE)),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// الأزرار
|
||||
_buildActionButtons(
|
||||
// 3. الأزرار (إجراءات)
|
||||
_buildCompactActionButtons(
|
||||
context, controller, profileController, audioController),
|
||||
|
||||
// إضافة هامش سفلي بسيط لرفع الأزرار عن حافة الشاشة
|
||||
const SizedBox(height: 5),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -384,56 +99,81 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
});
|
||||
}
|
||||
|
||||
// ويدجت معلومات السائق والسعر
|
||||
Widget _buildDriverAndPriceSection(MapPassengerController controller) {
|
||||
// --- الهيدر (بدون تغيير، ممتاز) ---
|
||||
Widget _buildCompactHeader(MapPassengerController controller) {
|
||||
return Row(
|
||||
children: [
|
||||
// صورة السائق
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: AppColor.primaryColor, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
color: AppColor.primaryColor.withOpacity(0.5), width: 1.5),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
radius: 24,
|
||||
backgroundImage: NetworkImage(
|
||||
'${AppLink.server}/portrate_captain_image/${controller.driverId}.jpg'),
|
||||
onBackgroundImageError: (_, __) => const Icon(Icons.person),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// الاسم والتقييم
|
||||
// الاسم ومعلومات السيارة
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
controller.driverName,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 18,
|
||||
color: Colors.black87,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.star_rounded,
|
||||
color: AppColor.yellowColor, size: 18),
|
||||
Flexible(
|
||||
child: Text(
|
||||
controller.driverName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
color: Colors.black87,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.star, color: Colors.amber, size: 14),
|
||||
Text(
|
||||
controller.driverRate,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
fontWeight: FontWeight.bold, color: Colors.grey[600]),
|
||||
style: const TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
'${controller.model} • ',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[700]),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
border: Border.all(color: Colors.black12),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
controller.licensePlate,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -441,27 +181,26 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// السعر المثبت (تصميم كارد للسعر)
|
||||
// السعر
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: AppColor.primaryColor.withOpacity(0.2)),
|
||||
color: AppColor.primaryColor.withOpacity(0.08),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text('Total'.tr,
|
||||
style: TextStyle(fontSize: 10, color: Colors.grey[600])),
|
||||
Text(
|
||||
'${NumberFormat('#,###').format(controller.totalPassenger)} 💰',
|
||||
NumberFormat('#,###').format(controller.totalPassenger),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 18,
|
||||
fontSize: 16,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
Text('SYP',
|
||||
style: TextStyle(fontSize: 9, color: Colors.grey[600])),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -469,163 +208,22 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// ويدجت معلومات السيارة ولوحة الأرقام
|
||||
Widget _buildCarInfoSection(MapPassengerController controller) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF9F9F9),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// نوع السيارة وموديلها
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${controller.make} ${controller.model}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Colors.black87),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
box.read(BoxName.carType) == 'Speed'
|
||||
? 'Fixed Price'
|
||||
.tr // سيظهر "سعر ثابت" (تأكد من إضافتها لملف الترجمة)
|
||||
: box.read(BoxName.carType) ?? 'Car',
|
||||
style: const TextStyle(fontSize: 12, color: Colors.black54),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// -------------------------------------------
|
||||
// تصميم لوحة السيارة الواقعي (Realistic Plate)
|
||||
// -------------------------------------------
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Colors.black, width: 2), // إطار أسود
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(2, 2)),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// الشريط الأزرق الجانبي (مثل اللوحات الدولية)
|
||||
Container(
|
||||
width: 15,
|
||||
height: 35, // ارتفاع اللوحة
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF003399), // أزرق غامق
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(2),
|
||||
bottomLeft: Radius.circular(2),
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
"SY", // رمز الدولة (مثال)
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// رقم اللوحة
|
||||
Text(
|
||||
controller.licensePlate, // رقم اللوحة من الكونترولر
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 2.0, // تباعد الأحرف لتبدو كأرقام محفورة
|
||||
fontFamily: 'monospace', // خط ثابت العرض ليشبه اللوحات
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTripProgress(MapPassengerController controller) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.access_time_filled,
|
||||
size: 16, color: Colors.grey),
|
||||
const SizedBox(width: 5),
|
||||
Text('Arriving in'.tr,
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 13)),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
controller.stringRemainingTimeRideBegin,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: LinearProgressIndicator(
|
||||
backgroundColor: Colors.grey[200],
|
||||
color: controller.remainingTimeTimerRideBegin < 60
|
||||
? AppColor.redColor
|
||||
: AppColor.primaryColor,
|
||||
minHeight: 8,
|
||||
value: controller.progressTimerRideBegin.toDouble(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButtons(
|
||||
// --- الأزرار (بدون تغيير) ---
|
||||
Widget _buildCompactActionButtons(
|
||||
BuildContext context,
|
||||
MapPassengerController controller,
|
||||
ProfileController profileController,
|
||||
AudioRecorderController audioController) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// زر SOS بتصميم تحذيري
|
||||
_buildActionButton(
|
||||
return SizedBox(
|
||||
height: 60,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_compactBtn(
|
||||
icon: Icons.sos_rounded,
|
||||
label: 'SOS',
|
||||
iconColor: Colors.white,
|
||||
bgColor: AppColor.redColor,
|
||||
label: 'SOS'.tr,
|
||||
color: AppColor.redColor,
|
||||
bgColor: AppColor.redColor.withOpacity(0.1),
|
||||
onTap: () async {
|
||||
if (box.read(BoxName.sosPhonePassenger) == null) {
|
||||
await profileController.updatField(
|
||||
@@ -635,123 +233,100 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
} else {
|
||||
makePhoneCall('112');
|
||||
}
|
||||
}),
|
||||
|
||||
// زر واتساب
|
||||
_buildActionButton(
|
||||
},
|
||||
),
|
||||
_compactBtn(
|
||||
icon: FontAwesome.whatsapp,
|
||||
label: 'WhatsApp',
|
||||
iconColor: Colors.white,
|
||||
bgColor: const Color(0xFF25D366),
|
||||
label: 'WhatsApp'.tr,
|
||||
color: const Color(0xFF25D366),
|
||||
bgColor: const Color(0xFF25D366).withOpacity(0.1),
|
||||
onTap: () async {
|
||||
if (box.read(BoxName.sosPhonePassenger) == null ||
|
||||
box.read(BoxName.sosPhonePassenger) == 'sos') {
|
||||
if (box.read(BoxName.sosPhonePassenger) == null) {
|
||||
await profileController.updatField(
|
||||
'sosPhone', TextInputType.phone);
|
||||
box.write(BoxName.sosPhonePassenger,
|
||||
profileController.prfoileData['sosPhone']);
|
||||
} else {
|
||||
final phoneNumber =
|
||||
box.read(BoxName.sosPhonePassenger).toString();
|
||||
final phone = controller.formatSyrianPhoneNumber(phoneNumber);
|
||||
final phone = controller.formatSyrianPhoneNumber(
|
||||
box.read(BoxName.sosPhonePassenger).toString());
|
||||
controller.sendWhatsapp(phone);
|
||||
}
|
||||
}),
|
||||
|
||||
// زر المشاركة
|
||||
_buildActionButton(
|
||||
icon: Icons.share_location_rounded,
|
||||
label: 'Share',
|
||||
iconColor: AppColor.primaryColor,
|
||||
},
|
||||
),
|
||||
_compactBtn(
|
||||
icon: Icons.share,
|
||||
label: 'Share'.tr,
|
||||
color: AppColor.primaryColor,
|
||||
bgColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
onTap: () async {
|
||||
await controller.shareTripWithFamily();
|
||||
}),
|
||||
|
||||
// زر التسجيل
|
||||
GetBuilder<AudioRecorderController>(
|
||||
init: audioController,
|
||||
builder: (audioCtx) {
|
||||
return _buildActionButton(
|
||||
icon: audioCtx.isRecording ? Icons.stop : Icons.mic,
|
||||
label: audioCtx.isRecording ? 'Stop' : 'Record',
|
||||
iconColor:
|
||||
audioCtx.isRecording ? Colors.white : AppColor.primaryColor,
|
||||
bgColor: audioCtx.isRecording
|
||||
? AppColor.redColor
|
||||
: AppColor.primaryColor.withOpacity(0.1),
|
||||
isRecordingAnimation: audioCtx.isRecording,
|
||||
onTap: () async {
|
||||
if (audioCtx.isRecording == false) {
|
||||
await audioCtx.startRecording();
|
||||
Toast.show(context, 'Start Record'.tr, AppColor.greenColor);
|
||||
} else {
|
||||
await audioCtx.stopRecording();
|
||||
Toast.show(context, 'Record saved'.tr, AppColor.greenColor);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// زر الشكوى
|
||||
_buildActionButton(
|
||||
icon: Icons.report_gmailerrorred_rounded,
|
||||
onTap: () async => await controller.shareTripWithFamily(),
|
||||
),
|
||||
GetBuilder<AudioRecorderController>(
|
||||
init: audioController,
|
||||
builder: (audioCtx) {
|
||||
return _compactBtn(
|
||||
icon: audioCtx.isRecording
|
||||
? Icons.stop_circle_outlined
|
||||
: Icons.mic_none_outlined,
|
||||
label: audioCtx.isRecording ? 'Stop'.tr : 'Record'.tr,
|
||||
color: audioCtx.isRecording
|
||||
? AppColor.redColor
|
||||
: AppColor.primaryColor,
|
||||
bgColor: audioCtx.isRecording
|
||||
? AppColor.redColor.withOpacity(0.1)
|
||||
: AppColor.primaryColor.withOpacity(0.1),
|
||||
onTap: () async {
|
||||
if (!audioCtx.isRecording) {
|
||||
await audioCtx.startRecording();
|
||||
Toast.show(context, 'Start Record'.tr, AppColor.greenColor);
|
||||
} else {
|
||||
await audioCtx.stopRecording();
|
||||
Toast.show(context, 'Record saved'.tr, AppColor.greenColor);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
_compactBtn(
|
||||
icon: Icons.info_outline_rounded,
|
||||
label: 'Report'.tr,
|
||||
iconColor: Colors.grey[700]!,
|
||||
color: Colors.grey[700]!,
|
||||
bgColor: Colors.grey[200]!,
|
||||
onTap: () {
|
||||
Get.to(() => ComplaintPage(), transition: Transition.downToUp);
|
||||
}),
|
||||
],
|
||||
onTap: () => Get.to(() => ComplaintPage()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
Widget _compactBtn({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color iconColor,
|
||||
required Color color,
|
||||
required Color bgColor,
|
||||
required VoidCallback onTap,
|
||||
bool isRecordingAnimation = false,
|
||||
}) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: 50,
|
||||
height: 50,
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: isRecordingAnimation
|
||||
? Border.all(color: AppColor.redColor, width: 2)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: bgColor.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 24),
|
||||
child: Icon(icon, size: 20, color: color),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black54,
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[700],
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,8 +151,8 @@ void showPaymentBottomSheet(BuildContext context) {
|
||||
_buildPaymentOption(
|
||||
context: context,
|
||||
controller: controller,
|
||||
amount: 10000,
|
||||
bonusAmount: 0,
|
||||
amount: 500,
|
||||
bonusAmount: 30,
|
||||
currency: 'SYP'.tr,
|
||||
),
|
||||
|
||||
@@ -160,8 +160,8 @@ void showPaymentBottomSheet(BuildContext context) {
|
||||
_buildPaymentOption(
|
||||
context: context,
|
||||
controller: controller,
|
||||
amount: 20000,
|
||||
bonusAmount: 500,
|
||||
amount: 1000,
|
||||
bonusAmount: 70,
|
||||
currency: 'SYP'.tr,
|
||||
),
|
||||
|
||||
@@ -169,8 +169,8 @@ void showPaymentBottomSheet(BuildContext context) {
|
||||
_buildPaymentOption(
|
||||
context: context,
|
||||
controller: controller,
|
||||
amount: 40000,
|
||||
bonusAmount: 2500,
|
||||
amount: 2000,
|
||||
bonusAmount: 180,
|
||||
currency: 'SYP'.tr,
|
||||
),
|
||||
|
||||
@@ -178,8 +178,8 @@ void showPaymentBottomSheet(BuildContext context) {
|
||||
_buildPaymentOption(
|
||||
context: context,
|
||||
controller: controller,
|
||||
amount: 100000,
|
||||
bonusAmount: 4000,
|
||||
amount: 5000,
|
||||
bonusAmount: 700,
|
||||
currency: 'SYP'.tr,
|
||||
),
|
||||
|
||||
@@ -466,18 +466,24 @@ void showPaymentOptions(BuildContext context, PaymentController controller) {
|
||||
Get.to(() => PaymentScreenSmsProvider(
|
||||
amount: double.parse(controller.selectedAmount.toString())));
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Pay by Sham Cash'.tr),
|
||||
const SizedBox(width: 10),
|
||||
Image.asset(
|
||||
'assets/images/shamCash.png',
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Pay by Sham Cash'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Image.asset(
|
||||
'assets/images/shamCash.png',
|
||||
width: 70,
|
||||
height: 70,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
|
||||
@@ -8,9 +8,8 @@ import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
// خدمة الدفع للراكب (تم تحديث المسارات)
|
||||
// --- خدمة الدفع (نفس المنطق السابق) ---
|
||||
class PaymentService {
|
||||
// المسار الجديد لمجلد الركاب
|
||||
final String _baseUrl = "${AppLink.paymentServer}/ride/shamcash/passenger";
|
||||
|
||||
Future<String?> createInvoice({required double amount}) async {
|
||||
@@ -19,7 +18,7 @@ class PaymentService {
|
||||
final response = await CRUD().postWallet(
|
||||
link: url,
|
||||
payload: {
|
||||
'passengerID': box.read(BoxName.passengerID), // استخدام passengerID
|
||||
'passengerID': box.read(BoxName.passengerID),
|
||||
'amount': amount.toString(),
|
||||
},
|
||||
).timeout(const Duration(seconds: 15));
|
||||
@@ -83,21 +82,46 @@ class PaymentScreenSmsProvider extends StatefulWidget {
|
||||
_PaymentScreenSmsProviderState();
|
||||
}
|
||||
|
||||
class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final PaymentService _paymentService = PaymentService();
|
||||
Timer? _pollingTimer;
|
||||
PaymentStatus _status = PaymentStatus.creatingInvoice;
|
||||
String? _invoiceNumber;
|
||||
|
||||
// العنوان الثابت للدفع (المستخرج من الصورة)
|
||||
final String _paymentAddress = "80f23afe40499b02f49966c3340ae0fc";
|
||||
|
||||
// متحكم الأنيميشن للوميض
|
||||
late AnimationController _blinkController;
|
||||
late Animation<Color?> _colorAnimation;
|
||||
late Animation<double> _shadowAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// إعداد الأنيميشن (وميض أحمر)
|
||||
_blinkController = AnimationController(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
vsync: this,
|
||||
)..repeat(reverse: true); // يكرر الحركة ذهاباً وإياباً
|
||||
|
||||
_colorAnimation = ColorTween(
|
||||
begin: Colors.red.shade700,
|
||||
end: Colors.red.shade100,
|
||||
).animate(_blinkController);
|
||||
|
||||
_shadowAnimation = Tween<double>(begin: 2.0, end: 15.0).animate(
|
||||
CurvedAnimation(parent: _blinkController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_createAndPollInvoice();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pollingTimer?.cancel();
|
||||
_blinkController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -216,7 +240,7 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
// 1. المبلغ
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 15),
|
||||
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.blue.shade800, Colors.blue.shade600]),
|
||||
@@ -224,97 +248,166 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.blue.withOpacity(0.25),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8))
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Text("المبلغ المطلوب",
|
||||
style: TextStyle(color: Colors.white70, fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 5),
|
||||
Text("${currencyFormat.format(widget.amount)} ل.س",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// 2. التعليمات والنسخ (للراكب)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.shade100,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
// 2. رقم البيان (هام جداً - وميض أحمر)
|
||||
AnimatedBuilder(
|
||||
animation: _blinkController,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: _colorAnimation.value ?? Colors.red,
|
||||
width: 3.0, // إطار سميك
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (_colorAnimation.value ?? Colors.red)
|
||||
.withOpacity(0.4),
|
||||
blurRadius: _shadowAnimation.value,
|
||||
spreadRadius: 2,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50, shape: BoxShape.circle),
|
||||
child: Icon(Icons.priority_high_rounded,
|
||||
color: Colors.orange.shade800, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
"انسخ الرقم أدناه وضعه في خانة (الملاحظات) عند الدفع.",
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.warning_rounded,
|
||||
color: Colors.red.shade800, size: 28),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"هام جداً: لا تنسَ!",
|
||||
style: TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w600)),
|
||||
color: Colors.red.shade900,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
"يجب نسخ (رقم البيان) هذا ووضعه في تطبيق شام كاش لضمان نجاح العملية.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: invoiceText));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: const Text("تم نسخ رقم البيان ✅",
|
||||
textAlign: TextAlign.center),
|
||||
backgroundColor: Colors.red.shade700));
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 15, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.red.shade200, width: 1)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("رقم البيان (Invoice No)",
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Colors.grey)),
|
||||
Text(invoiceText,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 2.0,
|
||||
color: Colors.red.shade900)),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(Icons.copy_rounded,
|
||||
color: Colors.red.shade900, size: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// 3. عنوان الدفع (اختياري / عادي)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("عنوان الدفع (Payment Address)",
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: invoiceText));
|
||||
Clipboard.setData(ClipboardData(text: _paymentAddress));
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: const Text("تم نسخ رقم البيان ✅",
|
||||
content: const Text("تم نسخ عنوان الدفع ✅",
|
||||
textAlign: TextAlign.center),
|
||||
backgroundColor: Colors.green.shade600));
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 15, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.blue.shade200, width: 1.5)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("رقم البيان (Invoice ID)",
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Colors.grey)),
|
||||
Text(invoiceText,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.5)),
|
||||
],
|
||||
),
|
||||
const Icon(Icons.copy_rounded,
|
||||
color: Colors.blue, size: 24),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(_paymentAddress,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'Courier',
|
||||
color: Colors.black87,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.copy, size: 18, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -322,10 +415,10 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// 3. QR Code
|
||||
const Text("امسح الرمز للدفع",
|
||||
// 4. QR Code
|
||||
const Text("أو امسح الرمز للدفع",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 15),
|
||||
const SizedBox(height: 10),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
@@ -336,30 +429,23 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
child: Image.asset(widget.qrImagePath))));
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(15),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
border: Border.all(color: Colors.grey.shade300)),
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(widget.qrImagePath,
|
||||
width: 180,
|
||||
height: 180,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (c, o, s) => const Icon(Icons.qr_code_2,
|
||||
size: 100, color: Colors.grey)),
|
||||
const SizedBox(height: 8),
|
||||
const Text("اضغط للتكبير",
|
||||
style: TextStyle(fontSize: 10, color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
child: Image.asset(widget.qrImagePath,
|
||||
width: 150,
|
||||
height: 150,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (c, o, s) => const Icon(Icons.qr_code_2,
|
||||
size: 100, color: Colors.grey)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
const SizedBox(height: 30),
|
||||
const LinearProgressIndicator(backgroundColor: Colors.white),
|
||||
const SizedBox(height: 10),
|
||||
const Text("ننتظر إشعار الدفع تلقائياً...",
|
||||
const Text("جاري التحقق من الدفع تلقائياً...",
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12)),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
@@ -405,7 +491,7 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
||||
const SizedBox(height: 15),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 30),
|
||||
child: Text("لم يصلنا إشعار الدفع.",
|
||||
child: Text("لم يصلنا إشعار الدفع خلال الوقت المحدد.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey))),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
@@ -106,7 +106,7 @@ class MyDialog extends GetxController {
|
||||
Get.back();
|
||||
},
|
||||
child: Text(
|
||||
'Cancel',
|
||||
'Cancel'.tr,
|
||||
style: TextStyle(
|
||||
color: AppColor.redColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
20
pubspec.lock
20
pubspec.lock
@@ -1212,10 +1212,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
version: "0.6.7"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1828,6 +1828,22 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
socket_io_client:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: socket_io_client
|
||||
sha256: "64bd271703db3682d4195dd813c555413d21a49bbaef7c3ed38932fd2a209a10"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
socket_io_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: socket_io_common
|
||||
sha256: "469c7e6bb0c8d571a5158c1352112654f03aedc2f0a246533e1cbdb41efa4937"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -78,6 +78,7 @@ dependencies:
|
||||
internet_connection_checker: ^3.0.1
|
||||
connectivity_plus: ^6.1.5
|
||||
app_links: ^6.4.1
|
||||
socket_io_client: ^1.0.2
|
||||
# flutter_map: ^8.2.2
|
||||
# latlong2: ^0.9.1
|
||||
# home_widget: ^0.7.0+1
|
||||
|
||||
Reference in New Issue
Block a user