26-1-21/1

This commit is contained in:
Hamza-Ayed
2026-01-21 17:01:45 +03:00
parent 11dfe94bbb
commit 3e89e1f1f0
32 changed files with 101957 additions and 12193 deletions

View File

@@ -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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

24
collect_code.py Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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` وحفظ التفضيل في التخزين المحلي.

View File

@@ -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";

View File

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

View File

@@ -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');
}
}
}

View File

@@ -78,7 +78,7 @@ class AudioRecorderController extends GetxController {
// Stop recording
Future<void> stopRecording() async {
await recorder.stop();
recorder.stop();
isRecording = false;
isPaused = false;
update();

View File

@@ -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);

View File

@@ -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");
}
}

View 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

View File

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

View File

@@ -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();
})
],
),

View File

@@ -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)),
),
),
),

View File

@@ -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();
},
);
}
}

View File

@@ -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),

View File

@@ -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])),
),
),
],
),
),
);
}
}

View File

@@ -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;

View File

@@ -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();
}
},

View File

@@ -147,7 +147,7 @@ class GoogleMapPassengerWidget extends StatelessWidget {
}
},
myLocationEnabled: true,
myLocationEnabled: false,
),
),
);

View File

@@ -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",

View File

@@ -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),
// ),
],
),
],

View File

@@ -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),
),
),
],
],
),
);
}
}

View File

@@ -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(

View File

@@ -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),

View File

@@ -106,7 +106,7 @@ class MyDialog extends GetxController {
Get.back();
},
child: Text(
'Cancel',
'Cancel'.tr,
style: TextStyle(
color: AppColor.redColor,
fontWeight: FontWeight.w600,

View File

@@ -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:

View File

@@ -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