diff --git a/siro_driver/lib/controller/auth/captin/register_captin_controller.dart b/siro_driver/lib/controller/auth/captin/register_captin_controller.dart index 97855e6..64282ae 100755 --- a/siro_driver/lib/controller/auth/captin/register_captin_controller.dart +++ b/siro_driver/lib/controller/auth/captin/register_captin_controller.dart @@ -17,6 +17,7 @@ import '../../../constant/colors.dart'; import '../../../views/auth/captin/ai_page.dart'; import '../../../views/auth/syria/registration_view.dart'; import '../../../views/home/Captin/home_captain/home_captin.dart'; +import '../../functions/country_logic.dart'; import '../../functions/sms_egypt_controller.dart'; class RegisterCaptainController extends GetxController { @@ -385,11 +386,13 @@ class RegisterCaptainController extends GetxController { if (formKey.currentState!.validate()) { isLoading = true; update(); + final fixedPhone = + CountryLogic.formatCurrentCountryPhone(phoneController.text); var res = await CRUD().post(link: AppLink.signUpCaptin, payload: { 'first_name': name.split(' ')[1], 'last_name': name.split(' ')[0], 'email': emailController.text, - 'phone': phoneController.text, + 'phone': fixedPhone, 'password': passwordController.text, 'gender': sex, 'site': address, diff --git a/siro_driver/lib/controller/firebase/firbase_messge.dart b/siro_driver/lib/controller/firebase/firbase_messge.dart index 9e57373..b1f87fd 100755 --- a/siro_driver/lib/controller/firebase/firbase_messge.dart +++ b/siro_driver/lib/controller/firebase/firbase_messge.dart @@ -211,9 +211,11 @@ class FirebaseMessagesController extends GetxController { if (Platform.isAndroid) { notificationController.showNotification(title, body, 'ding', ''); } - MyDialog().getDialog(title, body, () { - // Empty callback, MyDialog already closes itself using pop(). - }); + MyDialog().getChatDialog( + title.isNotEmpty ? title : 'message From passenger'.tr, + body, + () {}, + ); break; case 'token change': diff --git a/siro_driver/lib/controller/functions/country_logic.dart b/siro_driver/lib/controller/functions/country_logic.dart index 1891cf4..9200dd8 100644 --- a/siro_driver/lib/controller/functions/country_logic.dart +++ b/siro_driver/lib/controller/functions/country_logic.dart @@ -83,17 +83,55 @@ class CountryLogic { /// Helper to format phone using the current country in box. static String formatCurrentCountryPhone(String phone) { - String cleanPhone = phone.replaceAll(RegExp(r'[ \-\(\)]'), '').trim(); - if (cleanPhone.startsWith('+963') || cleanPhone.startsWith('00963')) { + String cleanPhone = phone.replaceAll(RegExp(r'[ \-\(\)\+]'), '').trim(); + + // 1. Explicit International Code Detection + if (cleanPhone.startsWith('00963')) { + cleanPhone = cleanPhone.replaceFirst('00963', '963'); + } + if (cleanPhone.startsWith('00962')) { + cleanPhone = cleanPhone.replaceFirst('00962', '962'); + } + if (cleanPhone.startsWith('0020')) { + cleanPhone = cleanPhone.replaceFirst('0020', '20'); + } + + if (cleanPhone.startsWith('963')) { return formatPhone(cleanPhone, 'Syria'); } - if (cleanPhone.startsWith('+20') || cleanPhone.startsWith('0020')) { + if (cleanPhone.startsWith('962')) { + return formatPhone(cleanPhone, 'Jordan'); + } + if (cleanPhone.startsWith('20')) { return formatPhone(cleanPhone, 'Egypt'); } - if (cleanPhone.startsWith('+962') || cleanPhone.startsWith('00962')) { + + // 2. Local/National Format Detection by Country-Specific Mobile Prefixes + // Jordan: 07x / 7x (9 national digits) + if (cleanPhone.startsWith('07') && cleanPhone.length == 10) { + return formatPhone(cleanPhone, 'Jordan'); + } + if (cleanPhone.startsWith('7') && cleanPhone.length == 9) { return formatPhone(cleanPhone, 'Jordan'); } + // Syria: 09x / 9x (9 national digits) + if (cleanPhone.startsWith('09') && cleanPhone.length == 10) { + return formatPhone(cleanPhone, 'Syria'); + } + if (cleanPhone.startsWith('9') && cleanPhone.length == 9) { + return formatPhone(cleanPhone, 'Syria'); + } + + // Egypt: 01x (10 national digits) / 1x (9 national digits) + if (cleanPhone.startsWith('01') && cleanPhone.length == 11) { + return formatPhone(cleanPhone, 'Egypt'); + } + if (cleanPhone.startsWith('1') && cleanPhone.length == 10) { + return formatPhone(cleanPhone, 'Egypt'); + } + + // 3. Fallback: Default to current user's country code saved in box final country = box.read(BoxName.countryCode) ?? 'Syria'; return formatPhone(cleanPhone, country); } diff --git a/siro_driver/lib/controller/functions/translate_helper.dart b/siro_driver/lib/controller/functions/translate_helper.dart new file mode 100644 index 0000000..bf4a2c0 --- /dev/null +++ b/siro_driver/lib/controller/functions/translate_helper.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class TranslateHelper { + static Future translateText(String text, String targetLang) async { + if (text.isEmpty) return text; + try { + final url = Uri.parse( + 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=$targetLang&dt=t&q=${Uri.encodeComponent(text)}' + ); + final response = await http.get(url); + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + if (decoded != null && decoded is List && decoded.isNotEmpty && decoded[0] is List) { + final List parts = decoded[0]; + String translated = ''; + for (var part in parts) { + if (part is List && part.isNotEmpty) { + translated += part[0].toString(); + } + } + return translated; + } + } + } catch (e) { + // Fallback to original text on any exception + } + return text; + } +} diff --git a/siro_driver/lib/controller/home/captin/order_request_controller.dart b/siro_driver/lib/controller/home/captin/order_request_controller.dart index 169ca95..f1fc346 100755 --- a/siro_driver/lib/controller/home/captin/order_request_controller.dart +++ b/siro_driver/lib/controller/home/captin/order_request_controller.dart @@ -67,12 +67,16 @@ class OrderRequestController extends GetxController String timeToPassenger = "Calculating...".tr; String distanceToPassenger = "--"; + String apiStartName = ""; + String apiEndName = ""; + // --- الخريطة --- Set polylines = {}; bool _hasCalculatedFullJourney = false; // حالة التطبيق والصوت bool isInBackground = false; + bool isAccepting = false; final AudioPlayer audioPlayer = AudioPlayer(); @override @@ -288,6 +292,15 @@ class OrderRequestController extends GetxController totalTripDuration = tripResult['duration_text']; polylines.add(tripResult['polyline']); + if (tripResult['start_name'] != null && + tripResult['start_name'].toString().isNotEmpty) { + apiStartName = tripResult['start_name'].toString(); + } + if (tripResult['end_name'] != null && + tripResult['end_name'].toString().isNotEmpty) { + apiEndName = tripResult['end_name'].toString(); + } + // 🔥 تخزين استجابة السيرفر كاملة (بما فيها الـ points والـ instructions) if (tripResult['raw_response'] != null) { box.write('cached_trip_route', tripResult['raw_response']); @@ -385,6 +398,8 @@ class OrderRequestController extends GetxController 'duration_text': durationText, 'polyline': polyline, 'encoded_polyline': encodedPoints, + 'start_name': data['startName']?.toString(), + 'end_name': data['endName']?.toString(), 'raw_response': response.body, // 🔥 نمرر الـ JSON كاملاً }; } @@ -618,91 +633,102 @@ class OrderRequestController extends GetxController // Accept Order Logic Future acceptOrder() async { + if (isAccepting) return; + isAccepting = true; + update(); + endTimer(); _stopAudio(); - // 1. إرسال الطلب - var res = await CRUD() - .post(link: "${AppLink.ride}/rides/acceptRide.php", payload: { - 'id': _safeGet(16), - 'rideTimeStart': DateTime.now().toString(), - 'status': 'Apply', - 'passengerToken': _safeGet(9), - 'driver_id': box.read(BoxName.driverID), - }); - - Log.print('res from orderrequestpage: ${res}'); - - // ============================================================ - // تصحيح: فحص الرد بدقة (Map أو String) - // ============================================================ - bool isFailure = false; - - if (res is Map && res['status'] == 'failure') { - isFailure = true; - } else if (res == 'failure') { - isFailure = true; - } - - if (isFailure) { - // ⛔ حالة الفشل: الطلب مأخوذ - MyDialog().getDialog( - "Sorry, the order was taken by another driver.".tr, '', () { - // بما أن MyDialog يغلق نفسه الآن، نحتاج Get.back() واحدة فقط لإغلاق صفحة الطلب - Get.back(); + try { + // 1. إرسال الطلب + var res = await CRUD() + .post(link: "${AppLink.ride}/rides/acceptRide.php", payload: { + 'id': _safeGet(16), + 'rideTimeStart': DateTime.now().toString(), + 'status': 'Apply', + 'passengerToken': _safeGet(9), + 'driver_id': box.read(BoxName.driverID), }); - } else { - // ✅ حالة النجاح - // حماية من الكراش: التأكد من وجود HomeCaptainController قبل استخدامه - if (!Get.isRegistered()) { - Get.put(HomeCaptainController()); - } else { - Get.find().changeRideId(); + Log.print('res from orderrequestpage: ${res}'); + + // ============================================================ + // تصحيح: فحص الرد بدقة (Map أو String) + // ============================================================ + bool isFailure = false; + + if (res is Map && res['status'] == 'failure') { + isFailure = true; + } else if (res == 'failure') { + isFailure = true; } - box.write(BoxName.statusDriverLocation, 'on'); - changeApplied(); + if (isFailure) { + // ⛔ حالة الفشل: الطلب مأخوذ + MyDialog().getDialog( + "Sorry, the order was taken by another driver.".tr, '', () { + // بما أن MyDialog يغلق نفسه الآن، نحتاج Get.back() واحدة فقط لإغلاق صفحة الطلب + Get.back(); + }); + } else { + // ✅ حالة النجاح - var rideArgs = { - 'passengerLocation': '${_safeGet(0)},${_safeGet(1)}', - 'passengerDestination': '${_safeGet(3)},${_safeGet(4)}', - 'Duration': totalTripDuration, - 'totalCost': _safeGet(26), - 'Distance': totalTripDistance, - 'name': _safeGet(8), - 'phone': _safeGet(10), - 'email': _safeGet(28), - 'WalletChecked': _safeGet(13), - 'tokenPassenger': _safeGet(9), - 'direction': - 'https://www.google.com/maps/dir/${_safeGet(0)}/${_safeGet(1)}/', - 'DurationToPassenger': timeToPassenger, - 'rideId': _safeGet(16), - 'passengerId': _safeGet(7), - 'driverId': _safeGet(18), - 'durationOfRideValue': totalTripDuration, - 'paymentAmount': _safeGet(2), - 'paymentMethod': _safeGet(13) == 'true' ? 'visa' : 'cash', - 'isHaveSteps': _safeGet(20), - 'step0': _safeGet(21), - 'step1': _safeGet(22), - 'step2': _safeGet(23), - 'step3': _safeGet(24), - 'step4': _safeGet(25), - 'passengerWalletBurc': _safeGet(26), - 'timeOfOrder': DateTime.now().toString(), - 'totalPassenger': _safeGet(2), - 'carType': _safeGet(31), - 'kazan': _safeGet(32), - 'startNameLocation': _safeGet(29), - 'endNameLocation': _safeGet(30), - }; + // حماية من الكراش: التأكد من وجود HomeCaptainController قبل استخدامه + if (!Get.isRegistered()) { + Get.put(HomeCaptainController()); + } else { + Get.find().changeRideId(); + } - box.write(BoxName.rideArguments, rideArgs); + box.write(BoxName.statusDriverLocation, 'on'); + changeApplied(); - // الانتقال النهائي - Get.off(() => PassengerLocationMapPage(), arguments: rideArgs); + var rideArgs = { + 'passengerLocation': '${_safeGet(0)},${_safeGet(1)}', + 'passengerDestination': '${_safeGet(3)},${_safeGet(4)}', + 'Duration': totalTripDuration, + 'totalCost': _safeGet(26), + 'Distance': totalTripDistance, + 'name': _safeGet(8), + 'phone': _safeGet(10), + 'email': _safeGet(28), + 'WalletChecked': _safeGet(13), + 'tokenPassenger': _safeGet(9), + 'direction': + 'https://www.google.com/maps/dir/${_safeGet(0)}/${_safeGet(1)}/', + 'DurationToPassenger': timeToPassenger, + 'rideId': _safeGet(16), + 'passengerId': _safeGet(7), + 'driverId': _safeGet(18), + 'durationOfRideValue': totalTripDuration, + 'paymentAmount': _safeGet(2), + 'paymentMethod': _safeGet(13) == 'true' ? 'visa' : 'cash', + 'isHaveSteps': _safeGet(20), + 'step0': _safeGet(21), + 'step1': _safeGet(22), + 'step2': _safeGet(23), + 'step3': _safeGet(24), + 'step4': _safeGet(25), + 'passengerWalletBurc': _safeGet(26), + 'timeOfOrder': DateTime.now().toString(), + 'totalPassenger': _safeGet(2), + 'carType': _safeGet(31), + 'kazan': _safeGet(32), + 'startNameLocation': _safeGet(29), + 'endNameLocation': _safeGet(30), + }; + + box.write(BoxName.rideArguments, rideArgs); + + // الانتقال النهائي + Get.off(() => PassengerLocationMapPage(), arguments: rideArgs); + } + } catch (e) { + Log.print("Error in acceptOrder: $e"); + } finally { + isAccepting = false; + update(); } } diff --git a/siro_driver/lib/main.dart b/siro_driver/lib/main.dart index e44d9a9..f2f4979 100755 --- a/siro_driver/lib/main.dart +++ b/siro_driver/lib/main.dart @@ -412,10 +412,21 @@ class _MyAppState extends State with WidgetsBindingObserver { } } + // ============================================================================== + // 🟢 دوال معالجة قبول الطلب وتجهيز المتغيرات 🟢 + // ============================================================================== + bool _isProcessingAccept = false; + // ============================================================================== // 🟢 دوال معالجة قبول الطلب وتجهيز المتغيرات 🟢 // ============================================================================== Future _processAcceptOrder(List data) async { + if (_isProcessingAccept) { + print("⏳ _processAcceptOrder: Already accepting order, skipping duplicate request."); + return; + } + _isProcessingAccept = true; + Get.dialog( WillPopScope( onWillPop: () async => false, @@ -485,6 +496,8 @@ class _MyAppState extends State with WidgetsBindingObserver { } print("❌ Error in accept process: $e"); Get.snackbar("خطأ", "حدث خطأ غير متوقع"); + } finally { + _isProcessingAccept = false; } } diff --git a/siro_driver/lib/views/home/Captin/driver_map_page.dart b/siro_driver/lib/views/home/Captin/driver_map_page.dart index 063b4f4..73354c9 100755 --- a/siro_driver/lib/views/home/Captin/driver_map_page.dart +++ b/siro_driver/lib/views/home/Captin/driver_map_page.dart @@ -1,13 +1,12 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:siro_driver/constant/box_name.dart'; import 'package:siro_driver/constant/style.dart'; import 'package:siro_driver/views/widgets/elevated_btn.dart'; import 'package:siro_driver/controller/home/captin/map_driver_controller.dart'; +import 'package:siro_driver/constant/currency.dart'; import '../../../constant/colors.dart'; import '../../../controller/functions/location_controller.dart'; -import '../../../main.dart'; import '../../Rate/rate_passenger.dart'; import '../../widgets/my_textField.dart'; import 'mapDriverWidgets/driver_end_ride_bar.dart'; @@ -15,7 +14,6 @@ import 'mapDriverWidgets/google_driver_map_page.dart'; import 'mapDriverWidgets/google_map_app.dart'; import 'mapDriverWidgets/passenger_info_window.dart'; import 'mapDriverWidgets/sos_connect.dart'; -import 'mapDriverWidgets/sped_circle.dart'; class PassengerLocationMapPage extends StatelessWidget { PassengerLocationMapPage({super.key}); @@ -409,7 +407,7 @@ class PricesWindow extends StatelessWidget { ), const SizedBox(height: 10), Text( - '${controller.totalCost} ${'\$'.tr}', + '${controller.totalCost} ${CurrencyHelper.currency}', style: AppStyle.headTitle2.copyWith( color: Theme.of(context).textTheme.bodyLarge?.color, fontSize: 42, diff --git a/siro_driver/lib/views/home/Captin/home_captain/home_captin.dart b/siro_driver/lib/views/home/Captin/home_captain/home_captin.dart index 8a6bdc2..04c418a 100755 --- a/siro_driver/lib/views/home/Captin/home_captain/home_captin.dart +++ b/siro_driver/lib/views/home/Captin/home_captain/home_captin.dart @@ -21,6 +21,7 @@ import '../../../../controller/home/navigation/navigation_view.dart'; import '../../../../controller/profile/setting_controller.dart'; import '../../../../env/env.dart'; import '../../../../main.dart'; +import '../../../../print.dart'; import '../../../notification/available_rides_page.dart'; import '../driver_map_page.dart'; import 'widget/connect.dart'; @@ -603,11 +604,13 @@ class _FloatingControls extends StatelessWidget { ), // Continue active ride if (box.read(BoxName.rideStatus) == 'Applied' || + box.read(BoxName.rideStatus) == 'Apply' || box.read(BoxName.rideStatus) == 'Begin') ...[ const SizedBox(height: 10), _Fab( onTap: () { - if (box.read(BoxName.rideStatus) == 'Applied') { + final status = box.read(BoxName.rideStatus); + if (status == 'Applied' || status == 'Apply') { Get.to(() => PassengerLocationMapPage(), arguments: box.read(BoxName.rideArguments)); Get.put(MapDriverController()) @@ -669,5 +672,27 @@ class _Fab extends StatelessWidget { // Helper // ───────────────────────────────────────────── Future checkForAppliedRide(BuildContext context) async { - checkForPendingOrderFromServer(); + if (Get.currentRoute == '/passenger-location-map') return; + + final localStatus = box.read(BoxName.rideStatus); + final localArgs = box.read(BoxName.rideArguments) ?? + box.read(BoxName.rideArgumentsFromBackground); + + if ((localStatus == 'Apply' || + localStatus == 'Applied' || + localStatus == 'Begin') && + localArgs != null && + localArgs != 'failure') { + Log.print("🔄 Auto-recovering active ride: status=$localStatus"); + if (localStatus == 'Apply' || localStatus == 'Applied') { + Get.to(() => PassengerLocationMapPage(), arguments: localArgs); + Get.put(MapDriverController()).changeRideToBeginToPassenger(); + } else if (localStatus == 'Begin') { + Get.to(() => PassengerLocationMapPage(), arguments: localArgs); + Get.put(MapDriverController()).startRideFromStartApp(); + } + } else { + // If no local active ride, check the server + await checkForPendingOrderFromServer(); + } } diff --git a/siro_driver/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart b/siro_driver/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart index a1b8034..a859190 100755 --- a/siro_driver/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart +++ b/siro_driver/lib/views/home/Captin/home_captain/widget/left_menu_map_captain.dart @@ -219,14 +219,16 @@ class _Divider extends StatelessWidget { // Server Helpers (unchanged logic) // ───────────────────────────────────────────── +bool _isCheckingPendingOrder = false; + Future checkForPendingOrderFromServer() async { - bool isProcessing = false; - if (isProcessing) return; + if (_isCheckingPendingOrder) return; + if (Get.currentRoute == '/passenger-location-map') return; final driverId = box.read(BoxName.driverID)?.toString(); if (driverId == null) return; - isProcessing = true; + _isCheckingPendingOrder = true; try { var response = await CRUD().post( @@ -258,7 +260,7 @@ Future checkForPendingOrderFromServer() async { } catch (_) { // silent } finally { - isProcessing = false; + _isCheckingPendingOrder = false; } } diff --git a/siro_driver/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart b/siro_driver/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart index b901a98..7d8a5d1 100755 --- a/siro_driver/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart +++ b/siro_driver/lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart @@ -15,6 +15,7 @@ import '../../../../controller/firebase/notification_service.dart'; import '../../../../controller/functions/crud.dart'; import '../../../../constant/links.dart'; import '../../../widgets/my_textField.dart'; +import '../../../../views/widgets/elevated_btn.dart'; class PassengerInfoWindow extends StatelessWidget { const PassengerInfoWindow({super.key}); @@ -251,7 +252,7 @@ class PassengerInfoWindow extends StatelessWidget { // --- الأزرار الرئيسية (وصلت / ابدأ) --- if (!controller.isRideBegin) - _buildActionButtons(controller), + _buildActionButtons(context, controller), // --- زر الإلغاء المدفوع (بعد انتهاء وقت الانتظار) --- if (controller.isdriverWaitTimeEnd && @@ -372,7 +373,7 @@ class PassengerInfoWindow extends StatelessWidget { ); } - Widget _buildActionButtons(MapDriverController controller) { + Widget _buildActionButtons(BuildContext context, MapDriverController controller) { if (controller.isArrivedSend) { return SizedBox( width: double.infinity, @@ -413,33 +414,33 @@ class PassengerInfoWindow extends StatelessWidget { // وقد يُسبب Get.back() اللاحق إغلاق صفحة الماب بدلاً من الـ loading dialog Get.defaultDialog( title: "Start Trip?".tr, - titleStyle: const TextStyle( + titleStyle: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, + color: AppColor.writeColor, ), middleText: "Ensure the passenger is in the car.".tr, + middleTextStyle: TextStyle( + fontSize: 14, + color: AppColor.grayColor, + ), + backgroundColor: Theme.of(context).cardColor, barrierDismissible: true, - radius: 14, - confirm: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF27AE60), - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10)), - ), + radius: 18, + confirm: MyElevatedButton( + title: 'Start'.tr, onPressed: () async { - // نُغلق الديالوج بـ Get.back() لضمان أن GetX يعرف أنه أُغلق Get.back(); - // ثم نُنفذ startRideFromDriver الذي يستخدم Get.dialog و Get.back بأمان await controller.startRideFromDriver(); }, - child: Text('Start'.tr, - style: const TextStyle(fontWeight: FontWeight.bold)), + kolor: AppColor.greenColor, ), cancel: TextButton( onPressed: () => Get.back(), - child: Text('Cancel'.tr, - style: const TextStyle(color: Colors.grey)), + child: Text( + 'Cancel'.tr, + style: TextStyle(color: AppColor.grayColor, fontWeight: FontWeight.w600), + ), ), ); }, diff --git a/siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart b/siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart index db9d9d6..dd05466 100755 --- a/siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart +++ b/siro_driver/lib/views/home/Captin/orderCaptin/order_over_lay.dart @@ -9,13 +9,12 @@ import 'package:siro_driver/constant/api_key.dart'; import 'package:siro_driver/models/overlay_service.dart'; import '../../../../constant/box_name.dart'; import '../../../../constant/links.dart'; -import '../../../../controller/firebase/firbase_messge.dart'; import '../../../../controller/firebase/local_notification.dart'; -import '../../../../controller/firebase/notification_service.dart'; import '../../../../controller/functions/crud.dart'; import '../../../../main.dart'; import '../../../../models/model/order_data.dart'; import '../../../../print.dart'; +import '../../../../constant/currency.dart'; // === Enhanced Colors for Better Readability === class AppColors { @@ -510,7 +509,7 @@ class _OrderOverlayState extends State flex: 3, child: _buildHighlightInfo( // FIX: Use the parsed priceValue here - "${NumberFormat('#,##0').format(priceValue)} ل.س", + "${NumberFormat('#,##0').format(priceValue)} ${CurrencyHelper.currency}", "السعر".tr, Icons.monetization_on_rounded, AppColors.priceHighlight, diff --git a/siro_driver/lib/views/home/Captin/orderCaptin/order_request_page.dart b/siro_driver/lib/views/home/Captin/orderCaptin/order_request_page.dart index 5154f64..d43f3e9 100755 --- a/siro_driver/lib/views/home/Captin/orderCaptin/order_request_page.dart +++ b/siro_driver/lib/views/home/Captin/orderCaptin/order_request_page.dart @@ -4,6 +4,7 @@ import 'package:intaleq_maps/intaleq_maps.dart'; import 'package:siro_driver/constant/api_key.dart'; import 'package:siro_driver/constant/colors.dart'; import 'package:siro_driver/controller/home/captin/order_request_controller.dart'; +import 'package:siro_driver/constant/currency.dart'; class OrderRequestPage extends StatelessWidget { const OrderRequestPage({super.key}); @@ -43,10 +44,12 @@ class OrderRequestPage extends StatelessWidget { final String passengerName = getValue(8).isEmpty ? "عميل" : getValue(8); - final String startAddr = - getValue(29).isEmpty ? "موقع الانطلاق" : getValue(29); - final String endAddr = - getValue(30).isEmpty ? "الوجهة" : getValue(30); + final String startAddr = controller.apiStartName.isNotEmpty + ? controller.apiStartName + : (getValue(29).isEmpty ? "موقع الانطلاق" : getValue(29)); + final String endAddr = controller.apiEndName.isNotEmpty + ? controller.apiEndName + : (getValue(30).isEmpty ? "الوجهة" : getValue(30)); final bool isVisa = (getValue(13) == 'true'); // منطق Speed = سعر ثابت @@ -194,7 +197,7 @@ class OrderRequestPage extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text("${controller.tripPrice} ل.س", + Text("${controller.tripPrice} ${CurrencyHelper.currency}", style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, @@ -294,12 +297,12 @@ class OrderRequestPage extends StatelessWidget { children: [ const Icon(Icons.my_location, size: 18, color: Colors.green), - Expanded( - child: Container( - width: 2, - color: Colors.grey.shade300, - margin: const EdgeInsets.symmetric( - vertical: 2))), + Container( + width: 2, + height: 38, + color: Colors.grey.shade300, + margin: const EdgeInsets.symmetric( + vertical: 4)), const Icon(Icons.location_on, size: 18, color: Colors.red), ], @@ -307,8 +310,6 @@ class OrderRequestPage extends StatelessWidget { const SizedBox(width: 15), Expanded( child: Column( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( @@ -319,15 +320,16 @@ class OrderRequestPage extends StatelessWidget { style: TextStyle( fontSize: 11, color: Colors.grey)), + const SizedBox(height: 2), Text(startAddr, style: const TextStyle( - fontSize: 14, + fontSize: 13, fontWeight: FontWeight.w600), - maxLines: 1, + maxLines: 2, overflow: TextOverflow.ellipsis), ], ), - const Spacer(), + const SizedBox(height: 14), Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -336,11 +338,12 @@ class OrderRequestPage extends StatelessWidget { style: TextStyle( fontSize: 11, color: Colors.grey)), + const SizedBox(height: 2), Text(endAddr, style: const TextStyle( - fontSize: 14, + fontSize: 13, fontWeight: FontWeight.w600), - maxLines: 1, + maxLines: 2, overflow: TextOverflow.ellipsis), ], ), @@ -373,7 +376,9 @@ class OrderRequestPage extends StatelessWidget { const SizedBox(width: 15), Expanded( child: ElevatedButton( - onPressed: () => controller.acceptOrder(), + onPressed: controller.isAccepting + ? null + : () => controller.acceptOrder(), style: ElevatedButton.styleFrom( backgroundColor: AppColor.primaryColor, foregroundColor: Colors.white, @@ -386,24 +391,35 @@ class OrderRequestPage extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text("قبول الرحلة", - style: TextStyle( + Text( + controller.isAccepting + ? "جاري القبول...".tr + : "قبول الرحلة".tr, + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(width: 15), SizedBox( width: 20, height: 20, - child: CircularProgressIndicator( - value: controller.progress, - color: Colors.white, - strokeWidth: 2.5, - backgroundColor: Colors.white24), + child: controller.isAccepting + ? const CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2.5, + ) + : CircularProgressIndicator( + value: controller.progress, + color: Colors.white, + strokeWidth: 2.5, + backgroundColor: Colors.white24), ), - const SizedBox(width: 8), - Text("${controller.remainingTime}", - style: const TextStyle( - fontSize: 14, color: Colors.white)), + if (!controller.isAccepting) ...[ + const SizedBox(width: 8), + Text("${controller.remainingTime}", + style: const TextStyle( + fontSize: 14, + color: Colors.white)), + ], ], ), ), diff --git a/siro_driver/lib/views/widgets/mydialoug.dart b/siro_driver/lib/views/widgets/mydialoug.dart index 5903124..457ecb5 100755 --- a/siro_driver/lib/views/widgets/mydialoug.dart +++ b/siro_driver/lib/views/widgets/mydialoug.dart @@ -6,6 +6,7 @@ import 'package:get/get.dart'; import 'package:siro_driver/constant/colors.dart'; import 'package:siro_driver/constant/style.dart'; import 'package:siro_driver/controller/functions/tts.dart'; +import 'package:siro_driver/controller/functions/translate_helper.dart'; class DialogConfig { static const Duration animationDuration = Duration(milliseconds: 200); @@ -136,6 +137,158 @@ class MyDialog extends GetxController { barrierColor: Colors.black.withAlpha(102), // 0.4 opacity ); } + + void getChatDialog(String title, String body, VoidCallback onPressed) { + final textToSpeechController = Get.put(TextToSpeechController()); + + HapticFeedback.mediumImpact(); + + String currentText = body; + bool isTranslated = false; + bool isLoading = false; + + Get.dialog( + StatefulBuilder( + builder: (context, setState) { + final theme = Theme.of(context); + return TweenAnimationBuilder( + duration: DialogConfig.animationDuration, + tween: Tween(begin: 0.0, end: 1.0), + builder: (context, value, child) { + return Transform.scale( + scale: 0.95 + (0.05 * value), + child: Opacity(opacity: value, child: child), + ); + }, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: DialogConfig.blurStrength, + sigmaY: DialogConfig.blurStrength, + ), + child: Builder(builder: (context) { + return CupertinoAlertDialog( + title: Column( + children: [ + Text( + title.tr, + style: AppStyle.title.copyWith( + fontSize: 20, + fontWeight: FontWeight.w700, + letterSpacing: -0.5, + color: AppColor.primaryColor, + ), + ), + const SizedBox(height: 8), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CupertinoButton( + padding: const EdgeInsets.all(8), + onPressed: () async { + HapticFeedback.selectionClick(); + await textToSpeechController.speakText(title); + await textToSpeechController.speakText(currentText); + }, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: + AppColor.primaryColor.withAlpha(26), // 0.1 opacity + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + CupertinoIcons.speaker_2_fill, + color: AppColor.primaryColor, + size: 24, + ), + ), + ), + const SizedBox(height: 8), + if (isLoading) + const Center( + child: Padding( + padding: EdgeInsets.all(8.0), + child: CupertinoActivityIndicator(), + ), + ) + else + Text( + currentText, + style: AppStyle.title.copyWith( + fontSize: 16, + height: 1.3, + color: theme.textTheme.bodyLarge?.color ?? AppColor.writeColor, + ), + textAlign: TextAlign.center, + ), + ], + ), + actions: [ + CupertinoDialogAction( + onPressed: () async { + if (isLoading) return; + HapticFeedback.lightImpact(); + if (isTranslated) { + setState(() { + currentText = body; + isTranslated = false; + }); + } else { + setState(() { + isLoading = true; + }); + try { + final targetLang = Get.locale?.languageCode ?? 'ar'; + final translated = await TranslateHelper.translateText(body, targetLang); + setState(() { + currentText = translated; + isTranslated = true; + isLoading = false; + }); + } catch (e) { + setState(() { + isLoading = false; + }); + } + } + }, + child: Text( + isTranslated ? 'Original'.tr : 'Translate'.tr, + style: TextStyle( + color: AppColor.blueColor, + fontWeight: FontWeight.w600, + fontSize: 17, + ), + ), + ), + CupertinoDialogAction( + onPressed: () { + HapticFeedback.mediumImpact(); + Navigator.of(context, rootNavigator: true).pop(); + onPressed(); + }, + child: Text( + 'OK'.tr, + style: TextStyle( + color: AppColor.greenColor, + fontWeight: FontWeight.w600, + fontSize: 17, + ), + ), + ), + ], + ); + }), + ), + ); + }, + ), + barrierDismissible: true, + barrierColor: Colors.black.withAlpha(102), // 0.4 opacity + ); + } } class MyDialogContent extends GetxController { diff --git a/siro_rider/lib/controller/auth/otp_controller.dart b/siro_rider/lib/controller/auth/otp_controller.dart index 97f4b2e..e3c7020 100644 --- a/siro_rider/lib/controller/auth/otp_controller.dart +++ b/siro_rider/lib/controller/auth/otp_controller.dart @@ -117,10 +117,11 @@ class PhoneAuthHelper { String? email, }) async { try { + final fixedPhone = CountryLogic.formatCurrentCountryPhone(phoneNumber); final response = await CRUD().post( link: _registerUrl, payload: { - 'phone_number': phoneNumber, + 'phone_number': fixedPhone, 'first_name': firstName, 'last_name': lastName, 'email': email ?? '', // Send empty string if null diff --git a/siro_rider/lib/controller/firebase/firbase_messge.dart b/siro_rider/lib/controller/firebase/firbase_messge.dart index 565a4ab..16e8b1d 100644 --- a/siro_rider/lib/controller/firebase/firbase_messge.dart +++ b/siro_rider/lib/controller/firebase/firbase_messge.dart @@ -19,6 +19,7 @@ import 'package:siro_rider/controller/voice_call_controller.dart'; import '../home/map/ride_lifecycle_controller.dart'; import '../home/map/ride_state.dart'; import 'local_notification.dart'; +import '../../views/widgets/mydialoug.dart'; class FirebaseMessagesController extends GetxController { final fcmToken = FirebaseMessaging.instance; @@ -171,14 +172,22 @@ class FirebaseMessagesController extends GetxController { if (Platform.isAndroid) { notificationController.showNotification(title, body, 'ding'); } - passengerDialog(body); + MyDialog().getChatDialog( + title.isNotEmpty ? title : 'message From passenger'.tr, + body, + () {}, + ); update(); } else if (category == 'message From Driver') { // <-- كان 'message From Driver' if (Platform.isAndroid) { notificationController.showNotification(title, body, 'ding'); } - passengerDialog(body); + MyDialog().getChatDialog( + title.isNotEmpty ? title : 'message From Driver'.tr, + body, + () {}, + ); update(); } else if (category == 'Trip is Begin') { // <-- كان 'Trip is Begin' diff --git a/siro_rider/lib/controller/functions/country_logic.dart b/siro_rider/lib/controller/functions/country_logic.dart index fdde578..c9adfab 100644 --- a/siro_rider/lib/controller/functions/country_logic.dart +++ b/siro_rider/lib/controller/functions/country_logic.dart @@ -81,17 +81,55 @@ class CountryLogic { /// Helper to format phone using the current country in box. static String formatCurrentCountryPhone(String phone) { - String cleanPhone = phone.replaceAll(RegExp(r'[ \-\(\)]'), '').trim(); - if (cleanPhone.startsWith('+963') || cleanPhone.startsWith('00963')) { + String cleanPhone = phone.replaceAll(RegExp(r'[ \-\(\)\+]'), '').trim(); + + // 1. Explicit International Code Detection + if (cleanPhone.startsWith('00963')) { + cleanPhone = cleanPhone.replaceFirst('00963', '963'); + } + if (cleanPhone.startsWith('00962')) { + cleanPhone = cleanPhone.replaceFirst('00962', '962'); + } + if (cleanPhone.startsWith('0020')) { + cleanPhone = cleanPhone.replaceFirst('0020', '20'); + } + + if (cleanPhone.startsWith('963')) { return formatPhone(cleanPhone, 'Syria'); } - if (cleanPhone.startsWith('+20') || cleanPhone.startsWith('0020')) { + if (cleanPhone.startsWith('962')) { + return formatPhone(cleanPhone, 'Jordan'); + } + if (cleanPhone.startsWith('20')) { return formatPhone(cleanPhone, 'Egypt'); } - if (cleanPhone.startsWith('+962') || cleanPhone.startsWith('00962')) { + + // 2. Local/National Format Detection by Country-Specific Mobile Prefixes + // Jordan: 07x / 7x (9 national digits) + if (cleanPhone.startsWith('07') && cleanPhone.length == 10) { + return formatPhone(cleanPhone, 'Jordan'); + } + if (cleanPhone.startsWith('7') && cleanPhone.length == 9) { return formatPhone(cleanPhone, 'Jordan'); } + // Syria: 09x / 9x (9 national digits) + if (cleanPhone.startsWith('09') && cleanPhone.length == 10) { + return formatPhone(cleanPhone, 'Syria'); + } + if (cleanPhone.startsWith('9') && cleanPhone.length == 9) { + return formatPhone(cleanPhone, 'Syria'); + } + + // Egypt: 01x (10 national digits) / 1x (9 national digits) + if (cleanPhone.startsWith('01') && cleanPhone.length == 11) { + return formatPhone(cleanPhone, 'Egypt'); + } + if (cleanPhone.startsWith('1') && cleanPhone.length == 10) { + return formatPhone(cleanPhone, 'Egypt'); + } + + // 3. Fallback: Default to current user's country code saved in box final country = box.read(BoxName.countryCode) ?? 'Jordan'; return formatPhone(cleanPhone, country); } diff --git a/siro_rider/lib/controller/functions/launch.dart b/siro_rider/lib/controller/functions/launch.dart index b0fc065..bd484e2 100644 --- a/siro_rider/lib/controller/functions/launch.dart +++ b/siro_rider/lib/controller/functions/launch.dart @@ -9,7 +9,7 @@ void showInBrowser(String url) async { } else {} } -Future makePhoneCall(String phoneNumber) async { +String cleanAndFormatPhoneNumber(String phoneNumber) { String formattedNumber = phoneNumber.replaceAll(RegExp(r'\s+'), ''); if (formattedNumber.length > 6) { @@ -18,6 +18,11 @@ Future makePhoneCall(String phoneNumber) async { formattedNumber = '+$formattedNumber'; } } + return formattedNumber; +} + +Future makePhoneCall(String phoneNumber) async { + String formattedNumber = cleanAndFormatPhoneNumber(phoneNumber); // ملاحظة: الأرقام القصيرة (مثل 112) ستتجاوز الشرط أعلاه وتبقى "112" وهو الصحيح // 3. التنفيذ (Launch) @@ -44,23 +49,26 @@ Future makePhoneCall(String phoneNumber) async { void launchCommunication( String method, String contactInfo, String message) async { + String formattedContact = cleanAndFormatPhoneNumber(contactInfo); + // WhatsApp prefers the phone number without the '+' prefix + String whatsappContact = formattedContact.replaceAll('+', ''); String url; if (Platform.isIOS) { switch (method) { case 'phone': - url = 'tel:$contactInfo'; + url = 'tel:$formattedContact'; break; case 'sms': - url = 'sms:$contactInfo?body=${Uri.encodeComponent(message)}'; + url = 'sms:$formattedContact?body=${Uri.encodeComponent(message)}'; break; case 'whatsapp': url = - 'https://api.whatsapp.com/send?phone=$contactInfo&text=${Uri.encodeComponent(message)}'; + 'https://api.whatsapp.com/send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}'; break; case 'email': url = - 'mailto:$contactInfo?subject=Subject&body=${Uri.encodeComponent(message)}'; + 'mailto:$formattedContact?subject=Subject&body=${Uri.encodeComponent(message)}'; break; default: return; @@ -68,11 +76,11 @@ void launchCommunication( } else if (Platform.isAndroid) { switch (method) { case 'phone': - url = 'tel:$contactInfo'; + url = 'tel:$formattedContact'; break; case 'sms': - url = 'sms:$contactInfo?body=${Uri.encodeComponent(message)}'; + url = 'sms:$formattedContact?body=${Uri.encodeComponent(message)}'; break; case 'whatsapp': // Check if WhatsApp is installed @@ -80,16 +88,16 @@ void launchCommunication( await canLaunchUrl(Uri.parse('whatsapp://')); if (whatsappInstalled) { url = - 'whatsapp://send?phone=$contactInfo&text=${Uri.encodeComponent(message)}'; + 'whatsapp://send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}'; } else { // Provide an alternative action, such as opening the WhatsApp Web API url = - 'https://api.whatsapp.com/send?phone=$contactInfo&text=${Uri.encodeComponent(message)}'; + 'https://api.whatsapp.com/send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}'; } break; case 'email': url = - 'mailto:$contactInfo?subject=Subject&body=${Uri.encodeComponent(message)}'; + 'mailto:$formattedContact?subject=Subject&body=${Uri.encodeComponent(message)}'; break; default: return; diff --git a/siro_rider/lib/controller/functions/translate_helper.dart b/siro_rider/lib/controller/functions/translate_helper.dart new file mode 100644 index 0000000..bf4a2c0 --- /dev/null +++ b/siro_rider/lib/controller/functions/translate_helper.dart @@ -0,0 +1,30 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class TranslateHelper { + static Future translateText(String text, String targetLang) async { + if (text.isEmpty) return text; + try { + final url = Uri.parse( + 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=$targetLang&dt=t&q=${Uri.encodeComponent(text)}' + ); + final response = await http.get(url); + if (response.statusCode == 200) { + final decoded = jsonDecode(response.body); + if (decoded != null && decoded is List && decoded.isNotEmpty && decoded[0] is List) { + final List parts = decoded[0]; + String translated = ''; + for (var part in parts) { + if (part is List && part.isNotEmpty) { + translated += part[0].toString(); + } + } + return translated; + } + } + } catch (e) { + // Fallback to original text on any exception + } + return text; + } +} diff --git a/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart b/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart index dd652fa..e126b1b 100644 --- a/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart +++ b/siro_rider/lib/views/home/map_widget.dart/apply_order_widget.dart @@ -293,7 +293,7 @@ class ApplyOrderWidget extends StatelessWidget { const Icon(Icons.star_rounded, color: Colors.amber, size: 14), Text( - " ${controller.driverRate} • ${controller.model}", + " ${controller.driverRate} • ${controller.model}${controller.carColor.isNotEmpty ? ' • ${controller.carColor.tr}' : ''}", style: TextStyle( fontSize: 12, color: Colors.grey[700], @@ -356,23 +356,25 @@ class ApplyOrderWidget extends StatelessWidget { final bool isBike = vehicleText.contains('scooter') || vehicleText.contains('bike') || vehicleText.contains('دراجة'); - 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( - isBike || - box.read(BoxName.carType) == 'Scooter' || - box.read(BoxName.carType) == 'Pink Bike' - ? 'assets/images/moto.png' - : 'assets/images/car3.png', - fit: BoxFit.contain, + return AnimatedCarIcon( + child: 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( + isBike || + box.read(BoxName.carType) == 'Scooter' || + box.read(BoxName.carType) == 'Pink Bike' + ? 'assets/images/moto.png' + : 'assets/images/car3.png', + fit: BoxFit.contain, + ), ), ), ); @@ -841,3 +843,48 @@ class TimeDriverToPassenger extends StatelessWidget { }); } } + +class AnimatedCarIcon extends StatefulWidget { + final Widget child; + const AnimatedCarIcon({Key? key, required this.child}) : super(key: key); + + @override + State createState() => _AnimatedCarIconState(); +} + +class _AnimatedCarIconState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + )..repeat(reverse: true); + + _animation = Tween( + begin: const Offset(-0.06, 0.0), + end: const Offset(0.06, 0.0), + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SlideTransition( + position: _animation, + child: widget.child, + ); + } +} diff --git a/siro_rider/lib/views/home/map_widget.dart/ride_begin_passenger.dart b/siro_rider/lib/views/home/map_widget.dart/ride_begin_passenger.dart index a1d43e3..7df7199 100644 --- a/siro_rider/lib/views/home/map_widget.dart/ride_begin_passenger.dart +++ b/siro_rider/lib/views/home/map_widget.dart/ride_begin_passenger.dart @@ -162,7 +162,7 @@ class RideBeginPassenger extends StatelessWidget { children: [ Flexible( child: Text( - '${controller.model} • ', + '${controller.model}${controller.carColor.isNotEmpty ? " • ${controller.carColor.tr}" : ""} • ', style: TextStyle(fontSize: 12, color: AppColor.grayColor), maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/siro_rider/lib/views/home/map_widget.dart/vip_begin.dart b/siro_rider/lib/views/home/map_widget.dart/vip_begin.dart index 9802a06..df74813 100644 --- a/siro_rider/lib/views/home/map_widget.dart/vip_begin.dart +++ b/siro_rider/lib/views/home/map_widget.dart/vip_begin.dart @@ -97,6 +97,15 @@ class VipRideBeginPassenger extends StatelessWidget { controller.model, style: AppStyle.title, ), + if (controller.carColor.isNotEmpty) ...[ + const SizedBox( + width: 10, + ), + Text( + controller.carColor.tr, + style: AppStyle.title, + ), + ], ], ), ), diff --git a/siro_rider/lib/views/widgets/mydialoug.dart b/siro_rider/lib/views/widgets/mydialoug.dart index cad3d5f..0be4e0b 100644 --- a/siro_rider/lib/views/widgets/mydialoug.dart +++ b/siro_rider/lib/views/widgets/mydialoug.dart @@ -7,6 +7,7 @@ import 'package:get/get.dart'; import '../../constant/colors.dart'; import '../../constant/style.dart'; import '../../controller/functions/tts.dart'; +import '../../controller/functions/translate_helper.dart'; // ───────────────────────────────────────────────────────────────────────────── // Config @@ -417,6 +418,167 @@ class MyDialog extends GetxController { barrierColor: _DC.barrierColor, ); } + + void getChatDialog( + String title, + String messageContent, + VoidCallback onPressed, { + IconData? icon, + }) { + HapticFeedback.mediumImpact(); + + String displayedText = messageContent; + bool isTranslated = false; + bool isLoading = false; + + Get.dialog( + StatefulBuilder( + builder: (dialogContext, setState) { + return _DialogShell( + child: _GlassCard( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ── Body ────────────────────────────────────────────── + Padding( + padding: const EdgeInsets.fromLTRB(24, 28, 24, 20), + child: Column( + children: [ + // Icon badge + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.primaryColor.withOpacity(0.1), + border: Border.all( + color: AppColor.primaryColor.withOpacity(0.2), + ), + ), + child: Icon( + icon ?? Icons.chat_bubble_outline_rounded, + color: AppColor.primaryColor, + size: 26, + ), + ), + const SizedBox(height: 16), + + // Title + Text( + title.tr, + textAlign: TextAlign.center, + style: AppStyle.title.copyWith( + fontSize: 18, + fontWeight: FontWeight.w700, + letterSpacing: -0.4, + color: AppColor.writeColor, + ), + ), + + const SizedBox(height: 10), + if (isLoading) + const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Center( + child: CupertinoActivityIndicator(radius: 12), + ), + ) + else + Text( + displayedText, + textAlign: TextAlign.center, + style: AppStyle.subtitle.copyWith( + fontSize: 14.5, + height: 1.5, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 16), + + // TTS button + _SpeakButton( + texts: [title.tr, displayedText], + ), + ], + ), + ), + + // ── Actions ─────────────────────────────────────────── + Container( + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: Colors.grey.withOpacity(0.15), width: 1), + ), + ), + child: Row( + children: [ + // Translate Toggle + Expanded( + child: _ActionButton( + label: isTranslated ? 'Original'.tr : 'Translate'.tr, + color: AppColor.blueColor, + backgroundColor: AppColor.blueColor.withOpacity(0.07), + onPressed: () async { + if (isLoading) return; + HapticFeedback.lightImpact(); + if (isTranslated) { + setState(() { + displayedText = messageContent; + isTranslated = false; + }); + } else { + setState(() { + isLoading = true; + }); + try { + final targetLang = Get.locale?.languageCode ?? 'ar'; + final translated = await TranslateHelper.translateText(messageContent, targetLang); + setState(() { + displayedText = translated; + isTranslated = true; + isLoading = false; + }); + } catch (e) { + setState(() { + isLoading = false; + }); + } + } + }, + isLeft: true, + ), + ), + Container(width: 1, height: 52, color: Colors.grey.withOpacity(0.15)), + // Confirm + Expanded( + child: _ActionButton( + label: 'OK'.tr, + color: AppColor.primaryColor, + backgroundColor: AppColor.primaryColor.withOpacity(0.07), + onPressed: () { + HapticFeedback.mediumImpact(); + Navigator.of(dialogContext, rootNavigator: true).pop(); + Future.delayed(const Duration(milliseconds: 100), () { + onPressed(); + }); + }, + isLeft: false, + isBold: true, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ), + barrierDismissible: true, + barrierColor: _DC.barrierColor, + ); + } } // ─────────────────────────────────────────────────────────────────────────────