From 97945aa3625bfef9b4a45e3aebd62f96b8e5ac8d Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Mon, 1 Jun 2026 23:36:27 +0300 Subject: [PATCH] Fixes & Updates - 2026-06-01: Integrate Back-End v3 updates, fix call/connection issues across apps --- .../reports/problems/problems-report.html | 2 +- compare.sh | 83 + compare_controllers.py | 79 + comparison_results.txt | 0 ios/Podfile | 5 + ios/Podfile.lock | 12 +- lib/app_bindings.dart | 25 + lib/constant/links.dart | 1 + lib/controller/auth/otp_controller.dart | 3 +- lib/controller/firebase/firbase_messge.dart | 60 +- lib/controller/functions/audio_record1.dart | 10 +- lib/controller/functions/log_out.dart | 17 + lib/controller/functions/upload_image.dart | 4 +- lib/controller/home/compare.sh | 89 + lib/controller/home/compare_precise.py | 104 + lib/controller/home/comparison_results.txt | 103 + lib/controller/home/map/car_location.dart | 15 + .../home/map/location_search_controller.dart | 1052 ++ .../home/map/map_engine_controller.dart | 809 + .../home/map/map_screen_binding.dart | 25 + .../home/map/map_socket_controller.dart | 326 + .../home/map/nearby_drivers_controller.dart | 475 + .../home/map/ride_lifecycle_controller.dart | 4558 +++++ lib/controller/home/map/ride_state.dart | 10 + .../home/map/ui_interactions_controller.dart | 436 + .../home/map_passenger_controller.dart | 15502 ++++++++-------- .../home/points_for_rider_controller.dart | 15 +- .../home/precise_comparison_results.txt | 159 + .../home/profile/complaint_controller.dart | 2 + lib/controller/home/vip_waitting_page.dart | 12 +- lib/controller/local/translations.dart | 11 + .../payment/payment_controller.dart | 18 +- lib/controller/rate/rate_conroller.dart | 18 +- lib/controller/voice_call_controller.dart | 722 + lib/homw_widget.dart | 19 - lib/services/signaling_service.dart | 111 + lib/views/Rate/rate_captain.dart | 6 +- lib/views/auth/otp_page.dart | 14 +- lib/views/auth/otp_token_page.dart | 8 +- lib/views/home/map_page_passenger.dart | 26 +- .../map_widget.dart/apply_order_widget.dart | 131 +- .../buttom_sheet_map_show.dart | 8 +- .../map_widget.dart/cancel_raide_page.dart | 6 +- .../car_details_widget_to_go.dart | 30 +- .../cash_confirm_bottom_page.dart | 4 +- .../driver_card_from_passenger.dart | 6 +- .../driver_time_arrive_passenger.dart | 4 +- .../form_search_places_destenation.dart | 231 +- .../map_widget.dart/form_search_start.dart | 225 +- .../form_serch_multiy_point.dart | 316 +- .../google_map_passenger_widget.dart | 29 +- .../map_widget.dart/left_main_menu_icons.dart | 137 +- .../map_widget.dart/main_bottom_Menu_map.dart | 1247 +- .../home/map_widget.dart/map_menu_widget.dart | 55 +- .../home/map_widget.dart/menu_map_page.dart | 4 +- .../passengerRideLoctionWidget.dart | 10 +- .../map_widget.dart/payment_method.page.dart | 5 +- .../picker_animation_container.dart | 40 +- .../points_page_for_rider.dart | 60 +- .../map_widget.dart/ride_begin_passenger.dart | 66 +- .../map_widget.dart/ride_from_start_app.dart | 26 +- .../searching_captain_window.dart | 19 +- .../select_driver_mishwari.dart | 16 +- ...timer_for_cancell_trip_from_passenger.dart | 6 +- .../timer_to_passenger_from_driver.dart | 4 +- lib/views/home/map_widget.dart/vip_begin.dart | 34 +- .../navigation/navigation_controller.dart | 82 +- .../home/navigation/navigation_view.dart | 1 + .../home/navigation/navigation_view_old.dart | 1613 -- .../widgets/voice_call_bottom_sheet.dart | 290 + .../flutter_paypal/lib/flutter_paypal.dart | 16 +- plan/intaleq_admin_analysis_report.md | 701 + pubspec.lock | 38 +- pubspec.yaml | 4 + scratch/benchmark_route.py | 201 + scratch/test_api.py | 17 + 76 files changed, 19806 insertions(+), 10822 deletions(-) create mode 100644 compare.sh create mode 100644 compare_controllers.py create mode 100644 comparison_results.txt create mode 100644 lib/controller/home/compare.sh create mode 100644 lib/controller/home/compare_precise.py create mode 100644 lib/controller/home/comparison_results.txt create mode 100644 lib/controller/home/map/car_location.dart create mode 100644 lib/controller/home/map/location_search_controller.dart create mode 100644 lib/controller/home/map/map_engine_controller.dart create mode 100644 lib/controller/home/map/map_screen_binding.dart create mode 100644 lib/controller/home/map/map_socket_controller.dart create mode 100644 lib/controller/home/map/nearby_drivers_controller.dart create mode 100644 lib/controller/home/map/ride_lifecycle_controller.dart create mode 100644 lib/controller/home/map/ride_state.dart create mode 100644 lib/controller/home/map/ui_interactions_controller.dart create mode 100644 lib/controller/home/precise_comparison_results.txt create mode 100644 lib/controller/voice_call_controller.dart delete mode 100644 lib/homw_widget.dart create mode 100644 lib/services/signaling_service.dart delete mode 100644 lib/views/home/navigation/navigation_view_old.dart create mode 100644 lib/views/widgets/voice_call_bottom_sheet.dart create mode 100644 plan/intaleq_admin_analysis_report.md create mode 100644 scratch/benchmark_route.py create mode 100644 scratch/test_api.py diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html index b3afce9..ad0a4cf 100644 --- a/android/build/reports/problems/problems-report.html +++ b/android/build/reports/problems/problems-report.html @@ -650,7 +650,7 @@ code + .copy-button { diff --git a/compare.sh b/compare.sh new file mode 100644 index 0000000..4f69352 --- /dev/null +++ b/compare.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +ORIG_FILE="lib/controller/home/map_passenger_controller.dart" +SPLIT_DIR="lib/controller/home/map" + +echo "Extracting methods from original controller..." +# Methods typically start with spaces and have patterns like: +# returnType methodName( or methodName( +# Let's extract words that precede ( on lines that don't start with keywords (if, for, while, switch, catch, etc.) +# We will use awk to parse. +METHODS=$(cat "$ORIG_FILE" | awk ' +# Skip single-line comments +/\/\// { next } +# Skip imports and class declarations +/import/ || /class/ { next } +# Find lines with "(" +/\(/ { + # Replace anything inside parentheses and curly braces to simplify + gsub(/\(.*\)/, "()") + # Find word before "()" + for (i = 1; i <= NF; i++) { + if ($i ~ /[a-zA-Z0-9_]+\(\)/) { + name = $i + sub(/\(\)/, "", name) + # Remove any leading modifiers like async, Future, static, etc. + # Only keep valid identifiers that are not control keywords + if (name !~ /^(if|for|while|switch|catch|super|await|print|assert|dynamic|void|return|with|override|get|set|else|try|final|const|var|late|static|factory|new|abstract|covariant|external|operator|part|required|typedef|yield)$/ && name ~ /^[a-zA-Z_][a-zA-Z0-9_]*$/) { + print name + } + } + } +}' | sort -u) + +echo "Extracting fields/variables from original controller..." +# Fields are usually declared inside the class at the beginning of lines or indented. +# e.g., RxBool isSearching = false.obs; or String? rideId; +VARS=$(cat "$ORIG_FILE" | awk ' +/\/\// { next } +/import/ || /class/ { next } +# Lines ending with ";" or containing "=" followed by ";" +/;/ { + # Extract words that look like declarations. + # We look for typical type names or var/final followed by variable name + for (i = 1; i < NF; i++) { + if ($i ~ /^(var|final|const|late|RxBool|RxInt|RxDouble|RxString|RxList|RxMap|RxSet|Rx|String|int|double|bool|List|Map|Set|Timer|LatLng|Position|IntaleqMapController)$/) { + # The next field might be the variable name, or it might have a type like String? + name = $(i+1) + # Remove trailing ?, ;, = + sub(/\?/, "", name) + sub(/;/, "", name) + sub(/=/, "", name) + if (name ~ /^[a-zA-Z_][a-zA-Z0-9_]*$/) { + print name + } + } + } +}' | sort -u) + +echo "Checking split files for methods..." +echo "--- MISSING METHODS ---" +MISSING_METHODS_COUNT=0 +for method in $METHODS; do + # Search for this method name as a whole word in split controllers + FOUND=$(grep -rw "$method" "$SPLIT_DIR" 2>/dev/null) + if [ -z "$FOUND" ]; then + echo " - $method" + MISSING_METHODS_COUNT=$((MISSING_METHODS_COUNT+1)) + fi +done +echo "Total missing methods: $MISSING_METHODS_COUNT" + +echo "" +echo "Checking split files for variables/fields..." +echo "--- MISSING VARIABLES ---" +MISSING_VARS_COUNT=0 +for var in $VARS; do + FOUND=$(grep -rw "$var" "$SPLIT_DIR" 2>/dev/null) + if [ -z "$FOUND" ]; then + echo " - $var" + MISSING_VARS_COUNT=$((MISSING_VARS_COUNT+1)) + fi +done +echo "Total missing variables: $MISSING_VARS_COUNT" diff --git a/compare_controllers.py b/compare_controllers.py new file mode 100644 index 0000000..569b552 --- /dev/null +++ b/compare_controllers.py @@ -0,0 +1,79 @@ +import os +import re + +original_path = 'lib/controller/home/map_passenger_controller.dart' +split_dir = 'lib/controller/home/map' + +# Read original file +with open(original_path, 'r', encoding='utf-8') as f: + orig_content = f.read() + +# Read all split files +split_contents = {} +for filename in os.listdir(split_dir): + if filename.endswith('.dart') and filename != 'map_screen_binding.dart': + filepath = os.path.join(split_dir, filename) + with open(filepath, 'r', encoding='utf-8') as f: + split_contents[filename] = f.read() + +# Combined content of all split files +combined_split_content = '\n'.join(split_contents.values()) + +# Regex to find method/function declarations inside a class in Dart +keywords = { + 'if', 'for', 'while', 'switch', 'catch', 'super', 'await', 'print', + 'assert', 'dynamic', 'void', 'return', 'with', 'override', 'get', 'set', + 'class', 'import', 'extends', 'implements', 'mixin', 'this', 'else', 'try', + 'final', 'const', 'var', 'late', 'static', 'factory', 'new', 'abstract', + 'covariant', 'external', 'operator', 'part', 'required', 'typedef', 'yield' +} + +def strip_comments(text): + text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) + text = re.sub(r'//.*', '', text) + return text + +orig_clean = strip_comments(orig_content) +combined_split_clean = strip_comments(combined_split_content) + +method_decl_pattern = re.compile( + r'(?:[a-zA-Z0-9_<>\?\[\]]+(?:\s+[a-zA-Z0-9_<>\?\[\]]+)*\s+)?([a-zA-Z0-9_]+)\s*\([^\)]*\)\s*(?:async)?\s*(?:=>|\{)' +) + +original_methods = set() +for match in method_decl_pattern.finditer(orig_clean): + method_name = match.group(1) + if method_name not in keywords and not method_name.isdigit(): + original_methods.add(method_name) + +var_decl_pattern = re.compile( + r'\b(?:var|final|const|late|Rx[a-zA-Z]+|String|int|double|bool|List|Map|Set|Timer|LatLng|Position|IntaleqMapController)\??\s+([a-zA-Z0-9_]+)\b' +) + +original_vars = set() +for match in var_decl_pattern.finditer(orig_clean): + var_name = match.group(1) + if var_name not in keywords and not var_name.isdigit(): + original_vars.add(var_name) + +missing_methods = [] +for method in sorted(original_methods): + if not re.search(r'\b' + re.escape(method) + r'\b', combined_split_clean): + missing_methods.append(method) + +missing_vars = [] +for var in sorted(original_vars): + if not re.search(r'\b' + re.escape(var) + r'\b', combined_split_clean): + missing_vars.append(var) + +print("--- MISSING METHODS ---") +print(f"Total original methods found: {len(original_methods)}") +print(f"Total missing: {len(missing_methods)}") +for m in missing_methods: + print(f" - {m}") + +print("\n--- MISSING VARIABLES/FIELDS ---") +print(f"Total original variables found: {len(original_vars)}") +print(f"Total missing: {len(missing_vars)}") +for v in missing_vars: + print(f" - {v}") diff --git a/comparison_results.txt b/comparison_results.txt new file mode 100644 index 0000000..e69de29 diff --git a/ios/Podfile b/ios/Podfile index 16d4165..f61c2cb 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -46,6 +46,11 @@ post_install do |installer| config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', 'PERMISSION_CONTACTS=1', + 'PERMISSION_LOCATION=1', + 'PERMISSION_MICROPHONE=1', + 'PERMISSION_NOTIFICATIONS=1', + 'PERMISSION_CAMERA=1', + 'PERMISSION_PHOTOS=1', ] end end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f133cdf..2b95974 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -81,6 +81,9 @@ PODS: - FlutterMacOS - flutter_tts (0.0.1): - Flutter + - flutter_webrtc (1.4.0): + - Flutter + - WebRTC-SDK (= 144.7559.01) - geolocator_apple (1.2.0): - Flutter - FlutterMacOS @@ -246,6 +249,7 @@ PODS: - FlutterMacOS - wakelock_plus (0.0.1): - Flutter + - WebRTC-SDK (144.7559.01) - webview_flutter_wkwebview (0.0.1): - Flutter - FlutterMacOS @@ -264,6 +268,7 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`) + - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) - image_cropper (from `.symlinks/plugins/image_cropper/ios`) @@ -322,6 +327,7 @@ SPEC REPOS: - StripePaymentsUI - StripeUICore - TOCropViewController + - WebRTC-SDK EXTERNAL SOURCES: app_links: @@ -350,6 +356,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" flutter_tts: :path: ".symlinks/plugins/flutter_tts/ios" + flutter_webrtc: + :path: ".symlinks/plugins/flutter_webrtc/ios" geolocator_apple: :path: ".symlinks/plugins/geolocator_apple/darwin" google_sign_in_ios: @@ -424,6 +432,7 @@ SPEC CHECKSUMS: flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 flutter_tts: 35ac3c7d42412733e795ea96ad2d7e05d0a75113 + flutter_webrtc: ec91d94b484ad49cf191ef93413f64a40ffd3b4c geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e google_sign_in_ios: 000870aa06da9b28d1d0bf7ef70ff0213059dd28 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 @@ -467,8 +476,9 @@ SPEC CHECKSUMS: vibration: ca8104a8875b9c493e15b21b04e456befd0ff6eb video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 + WebRTC-SDK: ab9b5319e458c2bfebdc92b3600740da35d5630d webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d -PODFILE CHECKSUM: 8a0b04ec79a0d49122ae6c10242e7cb023122802 +PODFILE CHECKSUM: 2ba2e4898a3d9a1615dafa81db0705bd67da92c3 COCOAPODS: 1.16.2 diff --git a/lib/app_bindings.dart b/lib/app_bindings.dart index 733fd35..ce6a2c0 100644 --- a/lib/app_bindings.dart +++ b/lib/app_bindings.dart @@ -5,6 +5,17 @@ import 'package:Intaleq/controller/firebase/local_notification.dart'; import 'package:Intaleq/controller/home/deep_link_controller.dart'; import 'package:Intaleq/controller/local/local_controller.dart'; import 'package:Intaleq/controller/functions/tts.dart'; +import 'package:Intaleq/controller/voice_call_controller.dart'; + +import 'package:Intaleq/controller/home/map/map_socket_controller.dart'; +import 'package:Intaleq/controller/home/map/map_engine_controller.dart'; +import 'package:Intaleq/controller/home/map/location_search_controller.dart'; +import 'package:Intaleq/controller/home/map/nearby_drivers_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart'; +import 'package:Intaleq/controller/home/map/ui_interactions_controller.dart'; +import 'package:Intaleq/controller/home/menu_controller.dart'; +import 'package:Intaleq/controller/functions/crud.dart'; +import 'package:Intaleq/controller/home/points_for_rider_controller.dart'; /// This is the central dependency injection file for the app. /// It uses GetX Bindings to make the app start faster and manage memory better. @@ -39,5 +50,19 @@ class AppBindings extends Bindings { // TextToSpeechController for global accessibility Get.lazyPut(() => TextToSpeechController(), fenix: true); + + // VoiceCallController for WebRTC calls + Get.lazyPut(() => VoiceCallController(), fenix: true); + + // Map & Ride controllers registered globally to prevent route-disposal race conditions. + Get.put(MapSocketController(), permanent: true); + Get.put(MapEngineController(), permanent: true); + Get.put(LocationSearchController(), permanent: true); + Get.put(NearbyDriversController(), permanent: true); + Get.put(RideLifecycleController(), permanent: true); + Get.put(UiInteractionsController(), permanent: true); + Get.put(MyMenuController(), permanent: true); + Get.put(CRUD(), permanent: true); + Get.put(WayPointController(), permanent: true); } } diff --git a/lib/constant/links.dart b/lib/constant/links.dart index bf9601c..146bd33 100644 --- a/lib/constant/links.dart +++ b/lib/constant/links.dart @@ -231,6 +231,7 @@ class AppLink { static String addMishwari = "$server/ride/mishwari/add.php"; static String cancelMishwari = "$server/ride/mishwari/cancel.php"; static String getMishwari = "$server/ride/mishwari/get.php"; + static String sendChatMessage = "$server/ride/chat/send_message.php"; //-----------------DriverOrder------------------ diff --git a/lib/controller/auth/otp_controller.dart b/lib/controller/auth/otp_controller.dart index d3cf572..5fe1150 100644 --- a/lib/controller/auth/otp_controller.dart +++ b/lib/controller/auth/otp_controller.dart @@ -107,13 +107,14 @@ class PhoneAuthHelper { /// Verifies the OTP and logs the user in. - static Future verifyOtp(String phoneNumber) async { + static Future verifyOtp(String phoneNumber, String otpCode) async { try { final fixedPhone = formatSyrianPhone(phoneNumber); final response = await CRUD().post( link: _verifyOtpUrl, payload: { 'phone_number': fixedPhone, + 'otp': otpCode, }, ); diff --git a/lib/controller/firebase/firbase_messge.dart b/lib/controller/firebase/firbase_messge.dart index e942fd1..47ab261 100644 --- a/lib/controller/firebase/firbase_messge.dart +++ b/lib/controller/firebase/firbase_messge.dart @@ -16,8 +16,9 @@ import '../../views/Rate/rate_captain.dart'; import '../../views/home/map_page_passenger.dart'; import '../../views/home/profile/promos_passenger_page.dart'; import '../auth/google_sign.dart'; -import '../functions/audio_record1.dart'; -import '../home/map_passenger_controller.dart'; +import 'package:Intaleq/controller/voice_call_controller.dart'; +import '../home/map/ride_lifecycle_controller.dart'; +import '../home/map/ride_state.dart'; import 'local_notification.dart'; class FirebaseMessagesController extends GetxController { @@ -105,8 +106,8 @@ class FirebaseMessagesController extends GetxController { // اقرأ "النوع" من حمولة البيانات، وليس من العنوان String category = message.data['category'] ?? ''; - final mapCtrl = Get.isRegistered() - ? Get.find() + final mapCtrl = Get.isRegistered() + ? Get.find() : null; // اقرأ العنوان (للعرض) String title = message.data['title'] ?? message.notification?.title ?? ''; @@ -167,8 +168,8 @@ class FirebaseMessagesController extends GetxController { GoogleSignInHelper.signOut(); } else if (category == 'Driver Is Going To Passenger') { // <-- كان 'Driver Is Going To Passenger' - Get.find().isDriverInPassengerWay = true; - Get.find().update(); + Get.find().isDriverInPassengerWay = true; + Get.find().update(); if (Platform.isAndroid) { notificationController.showNotification(title, body, 'tone1'); } @@ -214,7 +215,7 @@ class FirebaseMessagesController extends GetxController { } if (driverList.isNotEmpty) { - Get.find() + Get.find() .processRideFinished(driverList, source: "FCM"); } } else if (category == 'Finish Monitor') { @@ -232,9 +233,9 @@ class FirebaseMessagesController extends GetxController { Log.print("🔔 FCM: Ride Cancelled by Driver received."); // لا داعي لكتابة منطق التنظيف هنا، الكنترولر يتكفل بكل شيء - if (Get.isRegistered()) { + if (Get.isRegistered()) { // استدعاء الحارس (سيتجاهل الأمر إذا كان السوكيت قد سبقه) - Get.find() + Get.find() .processRideCancelledByDriver(message.data, source: "FCM"); } @@ -247,6 +248,19 @@ class FirebaseMessagesController extends GetxController { // ... (باقي الحالات مثل Call Income, Call End, إلخ) ... // ... بنفس الطريقة ... + else if (category == 'incoming_call') { + final sessionId = message.data['session_id']; + final callerName = message.data['caller_name']; + final rideId = message.data['ride_id']; + if (sessionId != null && callerName != null && rideId != null) { + Get.find().receiveCall( + sessionIdVal: sessionId.toString(), + remoteNameVal: callerName.toString(), + rideIdVal: rideId.toString(), + ); + } + } + else if (category == 'Order Applied') { if (Platform.isAndroid) { notificationController.showNotification( @@ -277,7 +291,7 @@ class FirebaseMessagesController extends GetxController { // var myList = jsonDecode(driverListJson) as List; // Log.print('myList: ${myList}'); - // final controller = Get.find(); + // final controller = Get.find(); // // استدعاء الدالة الموحدة الجديدة التي أنشأناها // await controller.processRideAcceptance( @@ -311,8 +325,8 @@ class FirebaseMessagesController extends GetxController { // } // GoogleSignInHelper.signOut(); // } else if (message.notification!.title! == 'Driver Is Going To Passenger') { - // Get.find().isDriverInPassengerWay = true; - // Get.find().update(); + // Get.find().isDriverInPassengerWay = true; + // Get.find().update(); // if (Platform.isAndroid) { // notificationController.showNotification('Driver is Going To You'.tr, // 'Please stay on the picked point.'.tr, 'tone1'); @@ -341,13 +355,13 @@ class FirebaseMessagesController extends GetxController { // // (تم حذف الإشعار المحلي من هنا، نُقل إلى الدالة الموحدة) - // final controller = Get.find(); + // final controller = Get.find(); // // استدعاء حارس البوابة الجديد والآمن // controller.processRideBegin(); // // (تم حذف كل الأوامر التالية من هنا) - // // Get.find().getBeginRideFromDriver(); + // // Get.find().getBeginRideFromDriver(); // // box.write(BoxName.passengerWalletTotal, '0'); // // update(); // } else if (message.notification!.title! == 'Hi ,I will go now'.tr) { @@ -360,7 +374,7 @@ class FirebaseMessagesController extends GetxController { // update(); // } // ... داخل معالج الإشعارات (FCM Handler) ... // if (message.notification!.title! == 'Hi ,I Arrive your site'.tr) { - // final controller = Get.find(); + // final controller = Get.find(); // // 1. التأكد أننا في الحالة الصحيحة (السائق كان في الطريق) // if (controller.currentRideState.value == RideState.driverApplied) { @@ -383,7 +397,7 @@ class FirebaseMessagesController extends GetxController { // title: 'Ok'.tr, // onPressed: () async { // Get.back(); - // await Get.find() + // await Get.find() // .reSearchAfterCanceledFromDriver(); // }, // ), @@ -394,7 +408,7 @@ class FirebaseMessagesController extends GetxController { // Get.offAll(() => const MapPagePassenger()); // }, // ) - // // Get.find() + // // Get.find() // // .searchNewDriverAfterRejectingFromDriver(); // ); // } else if (message.notification!.title! == 'Driver Finish Trip'.tr) { @@ -434,7 +448,7 @@ class FirebaseMessagesController extends GetxController { // box.write(BoxName.passengerWalletTotal, 0); // } - // Get.find().tripFinishedFromDriver(); + // Get.find().tripFinishedFromDriver(); // NotificationController().showNotification( // 'Don’t forget your personal belongings.'.tr, @@ -535,7 +549,7 @@ class FirebaseMessagesController extends GetxController { // box.write(BoxName.parentTripSelected, false); // box.remove(BoxName.tokenParent); - // Get.find().restCounter(); + // Get.find().restCounter(); // Get.offAll(() => const MapPagePassenger()); // } // // else if (message.notification!.title! == 'Order Applied') { @@ -595,8 +609,8 @@ class FirebaseMessagesController extends GetxController { // Get.find().sendNotificationToPassengerToken( // 'Hi ,I will go now'.tr, // 'I will go now'.tr, - // Get.find().driverToken, []); - // Get.find() + // Get.find().driverToken, []); + // Get.find() // .startTimerDriverWaitPassenger5Minute(); Get.back(); @@ -639,12 +653,12 @@ class DriverTipWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return GetBuilder(builder: (controller) { + return GetBuilder(builder: (controller) { return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // Text( - // '${'Your fee is '.tr}${Get.find().totalPassenger.toStringAsFixed(2)}'), + // '${'Your fee is '.tr}${Get.find().totalPassenger.toStringAsFixed(2)}'), Text( 'Do you want to pay Tips for this Driver'.tr, textAlign: TextAlign.center, diff --git a/lib/controller/functions/audio_record1.dart b/lib/controller/functions/audio_record1.dart index 095b8c8..8042cbc 100644 --- a/lib/controller/functions/audio_record1.dart +++ b/lib/controller/functions/audio_record1.dart @@ -30,7 +30,7 @@ class AudioRecorderController extends GetxController { } // Start recording - Future startRecording() async { + Future startRecording({String? rideId}) async { final bool isPermissionGranted = await recorder.hasPermission(); if (!isPermissionGranted) { // RecordingPermissionException('l'); @@ -38,10 +38,12 @@ class AudioRecorderController extends GetxController { } final directory = await getApplicationDocumentsDirectory(); + final String dateStr = + '${DateTime.now().year}-${DateTime.now().month.toString().padLeft(2, '0')}-${DateTime.now().day.toString().padLeft(2, '0')}'; // Generate a unique file name using the current timestamp - String fileName = - // '${DateTime.now().year}-${DateTime.now().month}-${DateTime.now().day}_${Get.find().rideId}.m4a'; - '${DateTime.now().year}-${DateTime.now().month}-${DateTime.now().day}.m4a'; + String fileName = (rideId != null && rideId.isNotEmpty && rideId != 'yet' && rideId != 'null') + ? '${dateStr}_$rideId.m4a' + : '$dateStr.m4a'; filePath = '${directory.path}/$fileName'; // Define the configuration for the recording diff --git a/lib/controller/functions/log_out.dart b/lib/controller/functions/log_out.dart index c0aa3cb..a8ae759 100644 --- a/lib/controller/functions/log_out.dart +++ b/lib/controller/functions/log_out.dart @@ -12,6 +12,14 @@ import 'package:Intaleq/views/widgets/elevated_btn.dart'; import 'package:Intaleq/views/widgets/my_textField.dart'; import '../../constant/style.dart'; +import 'package:Intaleq/controller/home/map/map_socket_controller.dart'; +import 'package:Intaleq/controller/home/map/map_engine_controller.dart'; +import 'package:Intaleq/controller/home/map/location_search_controller.dart'; +import 'package:Intaleq/controller/home/map/nearby_drivers_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart'; +import 'package:Intaleq/controller/home/map/ui_interactions_controller.dart'; +import 'package:Intaleq/controller/home/menu_controller.dart'; +import 'package:Intaleq/controller/home/points_for_rider_controller.dart'; class LogOutController extends GetxController { TextEditingController checkTxtController = TextEditingController(); @@ -110,6 +118,15 @@ class LogOutController extends GetxController { box.remove(BoxName.accountIdStripeConnect); box.remove(BoxName.passengerWalletTotal); box.remove(BoxName.isVerified); + Get.delete(force: true); + Get.delete(force: true); + Get.delete(force: true); + Get.delete(force: true); + Get.delete(force: true); + Get.delete(force: true); + Get.delete(force: true); + Get.delete(force: true); + Get.delete(force: true); Get.offAll(OnBoardingPage()); }, child: Text( diff --git a/lib/controller/functions/upload_image.dart b/lib/controller/functions/upload_image.dart index a3bfdcd..80f5b82 100644 --- a/lib/controller/functions/upload_image.dart +++ b/lib/controller/functions/upload_image.dart @@ -76,9 +76,11 @@ class ImageController extends GetxController { length, filename: basename(file.path), ); + final String fingerPrint = box.read(BoxName.deviceFpEncrypted)?.toString() ?? ''; request.headers.addAll({ 'Authorization': - 'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}' + 'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}', + 'X-Device-FP': fingerPrint, }); // Set the file name to the driverID request.files.add( diff --git a/lib/controller/home/compare.sh b/lib/controller/home/compare.sh new file mode 100644 index 0000000..02ae383 --- /dev/null +++ b/lib/controller/home/compare.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +ORIG_FILE="lib/controller/home/map_passenger_controller.dart" +ALL_FILES="lib/controller/home/map/location_search_controller.dart lib/controller/home/map/map_engine_controller.dart lib/controller/home/map/map_screen_binding.dart lib/controller/home/map/map_socket_controller.dart lib/controller/home/map/nearby_drivers_controller.dart lib/controller/home/map/ride_lifecycle_controller.dart lib/controller/home/map/ui_interactions_controller.dart" + +echo "Extracting methods from original controller..." +# Methods typically start with spaces and have patterns like: +# returnType methodName( or methodName( +# Let's extract words that precede ( on lines that don't start with keywords (if, for, while, switch, catch, etc.) +# We will use awk to parse. +METHODS=$(cat "$ORIG_FILE" | awk ' +# Skip single-line comments +/\/\// { next } +# Skip imports and class declarations +/import/ || /class/ { next } +# Find lines with "(" +/\(/ { + # Replace anything inside parentheses and curly braces to simplify + gsub(/\(.*\)/, "()") + # Find word before "()" + for (i = 1; i <= NF; i++) { + if ($i ~ /[a-zA-Z0-9_]+\(\)/) { + name = $i + sub(/\(\)/, "", name) + # Remove any leading modifiers like async, Future, static, etc. + # Only keep valid identifiers that are not control keywords + if (name !~ /^(if|for|while|switch|catch|super|await|print|assert|dynamic|void|return|with|override|get|set|else|try|final|const|var|late|static|factory|new|abstract|covariant|external|operator|part|required|typedef|yield)$/ && name ~ /^[a-zA-Z_][a-zA-Z0-9_]*$/) { + print name + } + } + } +}' | sort -u) + +echo "Extracting fields/variables from original controller..." +# Fields are usually declared inside the class at the beginning of lines or indented. +# e.g., RxBool isSearching = false.obs; or String? rideId; +VARS=$(cat "$ORIG_FILE" | awk ' +/\/\// { next } +/import/ || /class/ { next } +# Lines ending with ";" or containing "=" followed by ";" +/;/ { + # Extract words that look like declarations. + # We look for typical type names or var/final followed by variable name + for (i = 1; i < NF; i++) { + if ($i ~ /^(var|final|const|late|RxBool|RxInt|RxDouble|RxString|RxList|RxMap|RxSet|Rx|String|int|double|bool|List|Map|Set|Timer|LatLng|Position|IntaleqMapController)$/) { + # The next field might be the variable name, or it might have a type like String? + name = $(i+1) + # Remove trailing ?, ;, = + sub(/\?/, "", name) + sub(/;/, "", name) + sub(/=/, "", name) + if (name ~ /^[a-zA-Z_][a-zA-Z0-9_]*$/) { + print name + } + } + } +}' | sort -u) + +echo "Checking split files for methods..." +echo "--- MISSING METHODS ---" +MISSING_METHODS_COUNT=0 +# Create a temporary file with all split contents to search efficiently +cat $ALL_FILES > lib/controller/home/temp_split_combined.txt + +for method in $METHODS; do + # Search for this method name as a whole word in split controllers + FOUND=$(grep -w "$method" lib/controller/home/temp_split_combined.txt 2>/dev/null) + if [ -z "$FOUND" ]; then + echo " - $method" + MISSING_METHODS_COUNT=$((MISSING_METHODS_COUNT+1)) + fi +done +echo "Total missing methods: $MISSING_METHODS_COUNT" + +echo "" +echo "Checking split files for variables/fields..." +echo "--- MISSING VARIABLES ---" +MISSING_VARS_COUNT=0 +for var in $VARS; do + FOUND=$(grep -w "$var" lib/controller/home/temp_split_combined.txt 2>/dev/null) + if [ -z "$FOUND" ]; then + echo " - $var" + MISSING_VARS_COUNT=$((MISSING_VARS_COUNT+1)) + fi +done +echo "Total missing variables: $MISSING_VARS_COUNT" + +# Clean up temp file +rm lib/controller/home/temp_split_combined.txt diff --git a/lib/controller/home/compare_precise.py b/lib/controller/home/compare_precise.py new file mode 100644 index 0000000..57ba26c --- /dev/null +++ b/lib/controller/home/compare_precise.py @@ -0,0 +1,104 @@ +import sys +import re + +def parse_stream(stream_text): + # Splits the stream by our custom file delimiters + files = {} + parts = re.split(r'=== FILE: (.*?) ===\n', stream_text) + + # The first part is the original monolithic file + if parts: + files['original'] = parts[0] + + for i in range(1, len(parts), 2): + filename = parts[i] + content = parts[i+1] if i+1 < len(parts) else "" + files[filename] = content + + return files + +def strip_comments(text): + text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL) + text = re.sub(r'//.*', '', text) + return text + +def extract_declarations(text): + clean = strip_comments(text) + + # Matches method/function declarations inside a class in Dart + # e.g., void myMethod(..., Future myMethod(..., myMethod(..., get myProp, set myProp + # We look for word followed by ( or get/set followed by word. + method_decl_pattern = re.compile( + r'(?:[a-zA-Z0-9_<>\?\[\]]+(?:\s+[a-zA-Z0-9_<>\?\[\]]+)*\s+)?([a-zA-Z0-9_]+)\s*\([^\)]*\)\s*(?:async)?\s*(?:=>|\{)' + ) + + methods = set() + for match in method_decl_pattern.finditer(clean): + method_name = match.group(1) + if method_name not in keywords and not method_name.isdigit(): + methods.add(method_name) + + # Also extract getters and setters + getset_pattern = re.compile(r'\b(?:get|set)\s+([a-zA-Z0-9_]+)\b') + for match in getset_pattern.finditer(clean): + name = match.group(1) + if name not in keywords: + methods.add(name) + + # Extract variables/fields declarations + var_decl_pattern = re.compile( + r'\b(?:var|final|const|late|RxBool|RxInt|RxDouble|RxString|RxList|RxMap|RxSet|Rx|String|int|double|bool|List|Map|Set|Timer|LatLng|Position|IntaleqMapController)\??\s+([a-zA-Z0-9_]+)\b' + ) + + variables = set() + for match in var_decl_pattern.finditer(clean): + var_name = match.group(1) + if var_name not in keywords and not var_name.isdigit(): + variables.add(var_name) + + return methods, variables + +keywords = { + 'if', 'for', 'while', 'switch', 'catch', 'super', 'await', 'print', + 'assert', 'dynamic', 'void', 'return', 'with', 'override', 'get', 'set', + 'class', 'import', 'extends', 'implements', 'mixin', 'this', 'else', 'try', + 'final', 'const', 'var', 'late', 'static', 'factory', 'new', 'abstract', + 'covariant', 'external', 'operator', 'part', 'required', 'typedef', 'yield' +} + +def main(): + stream_text = sys.stdin.read() + files = parse_stream(stream_text) + + orig_content = files.get('original', '') + split_contents = {k: v for k, v in files.items() if k != 'original'} + + orig_methods, orig_vars = extract_declarations(orig_content) + + # Combined declarations in split files + split_methods = set() + split_vars = set() + for filename, content in split_contents.items(): + m, v = extract_declarations(content) + split_methods.update(m) + split_vars.update(v) + + missing_methods = sorted(orig_methods - split_methods) + missing_vars = sorted(orig_vars - split_vars) + + print("--- PRECISE MISSING METHODS ---") + print(f"Total original methods/getters/setters: {len(orig_methods)}") + print(f"Total defined in split controllers: {len(split_methods)}") + print(f"Total missing: {len(missing_methods)}") + for m in missing_methods: + print(f" - {m}") + + print("\n--- PRECISE MISSING VARIABLES/FIELDS ---") + print(f"Total original variables: {len(orig_vars)}") + print(f"Total defined in split controllers: {len(split_vars)}") + print(f"Total missing: {len(missing_vars)}") + for v in missing_vars: + print(f" - {v}") + +if __name__ == '__main__': + main() diff --git a/lib/controller/home/comparison_results.txt b/lib/controller/home/comparison_results.txt new file mode 100644 index 0000000..f554094 --- /dev/null +++ b/lib/controller/home/comparison_results.txt @@ -0,0 +1,103 @@ +Extracting methods from original controller... +Extracting fields/variables from original controller... +Checking split files for methods... +--- MISSING METHODS --- + - _applyLowEndModeIfNeeded + - _buildOsrmWaypointCoords + - _calculateDistance + - _checkAndRecalculateIfDeviated + - _fillDriverDataLocally + - _haversineKm + - _initMinimalIcons + - _initializePolygons + - _isActiveRideState + - _kmToLatDelta + - _kmToLngDelta + - _onDriverArrivedWithSocket + - _onRideCancelledWithSocket + - _onRideStartedWithSocket + - _relevanceScore + - _restorePolyline + - _stageNiceToHave + - _stagePricingAndState + - _startMasterTimer + - _startMasterTimerWithInterval + - _startPollingFallback + - _stopDriverLocationPolling + - _updateDriverMarker + - cancelRide + - detectPerfMode + - getAIKey + - getMapPointsForAllMethods + - getPassengerLocationUniversity + - handleActiveRideOnStartup + - isDriversDataValid + - onChangedPassengerCount + - onChangedPassengersChoose + - showDrawingBottomSheet + - showNoDriversDialog + - startSearchingTimer +Total missing methods: 35 + +Checking split files for variables/fields... +--- MISSING VARIABLES --- + - _isStateProcessing + - _isUsingFallback + - _maxReconnectAttempts + - apiDistanceMeters + - c + - carInfo + - carsOrder + - coordDestination + - currentCarType + - currentDriverLocation + - currentLocationOfDrivers + - currentRideId + - currentTimeSearchingCaptainWindow + - dInfo + - dLat + - datadriverCarsLocationToPassengerAfterApplied + - distanceOfTrip + - driverCarPlate + - driverLocationToPassenger + - driverOrderStatus + - durationByPassenger + - endLocation + - fName + - finalReason + - headingList + - increaseFeeFormKey + - isDriversTokensSend + - isFirstWaypoint + - isInUniversity + - isSaaSRequest + - kmInDegree + - lName + - latDest + - latestPosition + - lngDest + - lowPerf + - messagesFormKey + - originCoords + - pLower + - passengerLocationStringUnvirsity + - previousLocationOfDrivers + - progressTimerRideBeginVip + - qLower + - rLat1 + - rLat2 + - ram + - rideData + - sdk + - selectedPassengerCount + - startLng + - startLocation + - stringElapsedTimeRideBegin + - tax + - totalPassengerBalashDiscount + - totalPassengerComfortDiscount + - totalPassengerElectricDiscount + - totalPassengerLadyDiscount + - totalPassengerRaihGaiDiscount + - totalPassengerSpeedDiscount +Total missing variables: 59 diff --git a/lib/controller/home/map/car_location.dart b/lib/controller/home/map/car_location.dart new file mode 100644 index 0000000..2c3ca3b --- /dev/null +++ b/lib/controller/home/map/car_location.dart @@ -0,0 +1,15 @@ +class CarLocation { + final String id; + final double latitude; + final double longitude; + final double distance; + final double duration; + + CarLocation({ + required this.id, + required this.latitude, + required this.longitude, + this.distance = 10000, + this.duration = 10000, + }); +} diff --git a/lib/controller/home/map/location_search_controller.dart b/lib/controller/home/map/location_search_controller.dart new file mode 100644 index 0000000..ff14928 --- /dev/null +++ b/lib/controller/home/map/location_search_controller.dart @@ -0,0 +1,1052 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math' show cos, pi, max, min, atan2, sin, sqrt; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import 'package:location/location.dart'; +import 'package:http/http.dart' as http; + +import '../../../constant/box_name.dart'; +import '../../../constant/links.dart'; +import '../../../constant/colors.dart'; +import '../../../constant/style.dart'; +import '../../../constant/table_names.dart'; +import '../../../main.dart'; // contains global 'box', 'sql' +import '../../../print.dart'; +import '../../../services/offline_map_service.dart'; +import '../../functions/crud.dart'; +import '../points_for_rider_controller.dart'; +import '../../../views/home/map_widget.dart/form_serch_multiy_point.dart'; +import '../../../views/widgets/error_snakbar.dart'; +import 'map_engine_controller.dart'; +import '../deep_link_controller.dart'; +import 'ride_lifecycle_controller.dart'; +import 'ride_state.dart'; +import '../../../constant/country_polygons.dart'; +import '../../../constant/univeries_polygon.dart'; + +class LocationSearchController extends GetxController { + List> waypoints = []; + String passengerLocationStringUnvirsity = 'Not in University'; + bool isInUniversity = false; + List coordinatesWithoutEmpty = []; + LatLng? latestPosition; + + List polylineCoordinates0 = []; + List polylineCoordinates1 = []; + List polylineCoordinates2 = []; + List polylineCoordinates3 = []; + List polylineCoordinates4 = []; + late List> polylineCoordinatesPointsAll = [ + polylineCoordinates0, + polylineCoordinates1, + polylineCoordinates2, + polylineCoordinates3, + polylineCoordinates4, + ]; + + bool isLoading = true; + TextEditingController placeDestinationController = TextEditingController(); + TextEditingController placeStartController = TextEditingController(); + TextEditingController wayPoint0Controller = TextEditingController(); + TextEditingController wayPoint1Controller = TextEditingController(); + TextEditingController wayPoint2Controller = TextEditingController(); + TextEditingController wayPoint3Controller = TextEditingController(); + TextEditingController wayPoint4Controller = TextEditingController(); + TextEditingController whatsAppLocationText = TextEditingController(); + final sosFormKey = GlobalKey(); + + List bounds = []; + List placesStart = []; + List allTextEditingPlaces = []; + List placesDestination = []; + List wayPoint0 = []; + List wayPoint1 = []; + List wayPoint2 = []; + List wayPoint3 = []; + List wayPoint4 = []; + List> placeListResponseAll = []; + + List placeListResponse = []; + + PermissionStatus? permissionGranted; + final location = Location(); + double speed = 0; + + late LatLng passengerLocation = const LatLng(32, 34); + late LatLng newMyLocation = const LatLng(32.115295, 36.064773); + late LatLng newStartPointLocation = const LatLng(32.115295, 36.064773); + late LatLng newPointLocation0 = const LatLng(32.115295, 36.064773); + late LatLng newPointLocation1 = const LatLng(32.115295, 36.064773); + late LatLng newPointLocation2 = const LatLng(32.115295, 36.064773); + late LatLng newPointLocation3 = const LatLng(32.115295, 36.064773); + late LatLng newPointLocation4 = const LatLng(32.115295, 36.064773); + late LatLng myDestination = const LatLng(32, 34); + + bool startLocationFromMap = false; + bool passengerStartLocationFromMap = false; + bool workLocationFromMap = false; + bool homeLocationFromMap = false; + bool isPassengerRideLocationWidget = false; + bool startLocationFromMap0 = false; + bool startLocationFromMap1 = false; + bool startLocationFromMap2 = false; + bool startLocationFromMap3 = false; + bool startLocationFromMap4 = false; + List startLocationFromMapAll = []; + + bool currentLocationToFormPlaces = false; + bool currentLocationToFormPlaces0 = false; + bool currentLocationToFormPlaces1 = false; + bool currentLocationToFormPlaces2 = false; + bool currentLocationToFormPlaces3 = false; + bool currentLocationToFormPlaces4 = false; + List currentLocationToFormPlacesAll = []; + + // Multi-Waypoint (max 2 stops) + List menuWaypoints = [null, null]; + List menuWaypointNames = ['', '']; + int activeMenuWaypointCount = 0; + bool isPickingWaypoint = false; + int pickingWaypointIndex = -1; + + int wayPointIndex = 0; + String hintTextStartPoint = 'Search for your Start point'.tr; + String hintTextwayPoint0 = 'Search for waypoint'.tr; + String hintTextwayPoint1 = 'Search for waypoint'.tr; + String hintTextwayPoint2 = 'Search for waypoint'.tr; + String hintTextwayPoint3 = 'Search for waypoint'.tr; + String hintTextwayPoint4 = 'Search for waypoint'.tr; + String currentLocationString = 'Current Location'.tr; + String currentLocationString0 = 'Current Location'.tr; + String currentLocationString1 = 'Add Location 1'.tr; + String currentLocationString2 = 'Add Location 2'.tr; + String currentLocationString3 = 'Add Location 3'.tr; + String currentLocationString4 = 'Add Location 4'.tr; + String placesCoordinate0 = ''.tr; + String placesCoordinate1 = ''.tr; + String placesCoordinate2 = ''.tr; + String placesCoordinate3 = ''.tr; + String placesCoordinate4 = ''.tr; + List currentLocationStringAll = []; + List hintTextwayPointStringAll = []; + var placesCoordinate = []; + String hintTextDestinationPoint = 'Select your destination'.tr; + + late String startNameAddress = ''; + late String endNameAddress = ''; + List> stopPoints = []; + + double latitudeWhatsApp = 0; + double longitudeWhatsApp = 0; + late List recentPlaces = []; + + Timer? _camThrottle; + + final DeepLinkController _deepLinkController = + Get.isRegistered() + ? Get.find() + : Get.put(DeepLinkController()); + + @override + void onInit() { + super.onInit(); + placeListResponse = [ + formSearchPlaces(0), + formSearchPlaces(1), + formSearchPlaces(2), + formSearchPlaces(3), + ]; + readyWayPoints(); + getLocation(); + } + + void readyWayPoints() { + hintTextwayPointStringAll = [ + hintTextwayPoint0, + hintTextwayPoint1, + hintTextwayPoint2, + hintTextwayPoint3, + hintTextwayPoint4, + ]; + allTextEditingPlaces = [ + wayPoint0Controller, + wayPoint1Controller, + wayPoint2Controller, + wayPoint3Controller, + wayPoint4Controller, + ]; + currentLocationToFormPlacesAll = [ + currentLocationToFormPlaces0, + currentLocationToFormPlaces1, + currentLocationToFormPlaces2, + currentLocationToFormPlaces3, + currentLocationToFormPlaces4, + ]; + placeListResponseAll = [ + wayPoint0, + wayPoint1, + wayPoint2, + wayPoint3, + wayPoint4 + ]; + startLocationFromMapAll = [ + startLocationFromMap0, + startLocationFromMap1, + startLocationFromMap2, + startLocationFromMap3, + startLocationFromMap4, + ]; + currentLocationStringAll = [ + currentLocationString0, + currentLocationString1, + currentLocationString2, + currentLocationString3, + currentLocationString4, + ]; + placesCoordinate = [ + placesCoordinate0, + placesCoordinate1, + placesCoordinate2, + placesCoordinate3, + placesCoordinate4, + ]; + update(); + } + + void removeStop(Map stop) { + stopPoints.remove(stop); + update(); + } + + Future savePlaceToServer( + String latitude, String longitude, String name, String rate) async { + var data = { + 'latitude': latitude, + 'longitude': longitude, + 'name': name, + 'rate': rate, + }; + try { + CRUD().post( + link: AppLink.savePlacesServer, + payload: data, + ); + } catch (e) { + Log.print('Error: $e'); + } + } + + Future getLocation() async { + Log.print('🛰️ getLocation() called'); + permissionGranted = await location.hasPermission(); + if (permissionGranted == PermissionStatus.denied) { + permissionGranted = await location.requestPermission(); + if (permissionGranted != PermissionStatus.granted) { + return; + } + } + + // Pre-populate with last known position for instant loading + try { + Position? lastPosition = await Geolocator.getLastKnownPosition(); + if (lastPosition != null) { + passengerLocation = + LatLng(lastPosition.latitude, lastPosition.longitude); + newStartPointLocation = passengerLocation; + newMyLocation = passengerLocation; + Log.print( + '📍 Pre-populated location from last known: $passengerLocation'); + } + } catch (e) { + Log.print('⚠️ Error getting last known position: $e'); + } + + LocationData? _locationData; + try { + _locationData = await location.getLocation().timeout( + const Duration(seconds: 5), + onTimeout: () { + Log.print("⚠️ Location fetch timed out after 5s."); + return LocationData.fromMap({ + "latitude": passengerLocation.latitude, + "longitude": passengerLocation.longitude, + "speed": 0.0 + }); + }, + ); + } catch (e) { + Log.print("⚠️ Error fetching location: $e"); + } + + if (_locationData == null) { + isLoading = false; + update(); + return; + } + passengerLocation = + (_locationData.latitude != null && _locationData.longitude != null + ? LatLng(_locationData.latitude!, _locationData.longitude!) + : const LatLng(32, 34)); + + newStartPointLocation = passengerLocation; + newMyLocation = passengerLocation; + + if (Get.isRegistered()) { + final rideLifecycle = Get.find(); + rideLifecycle.getLocationArea( + passengerLocation.latitude, passengerLocation.longitude); + rideLifecycle.resetNoRideSearch(); + } + + try { + getReverseGeocoding(passengerLocation).then((address) { + currentLocationString = address; + update(); + }); + } catch (e) { + Log.print('Error resolving current location: $e'); + } + + OfflineMapService.instance + .downloadRegion(passengerLocation, radiusKm: 10.0); + + try { + final mapEngine = Get.find(); + if (mapEngine.mapController != null) { + mapEngine.mapController!.animateCamera( + CameraUpdate.newLatLng(passengerLocation), + ); + } + } catch (e) { + Log.print('Error animating camera to passenger location: $e'); + } + + speed = _locationData.speed ?? 0.0; + isLoading = false; + update(); + } + + void getCurrentLocationFormString() async { + currentLocationToFormPlaces = true; + currentLocationString = 'Waiting for your location'.tr; + await getLocation(); + currentLocationString = passengerLocation.toString(); + newStartPointLocation = passengerLocation; + update(); + } + + Future getPlaces() async { + final q = placeDestinationController.text.trim(); + if (q.isEmpty || q.length < 3) { + placesDestination = []; + update(); + return; + } + + final lat = passengerLocation.latitude; + final lng = passengerLocation.longitude; + final country = CountryPolygons.getCountryName(passengerLocation); + + try { + final url = + '${AppLink.searchGeocoding}?q=${Uri.encodeComponent(q)}&lat=$lat&lng=$lng&radius=15000&country=$country'; + final response = await CRUD().getMapSaas(link: url); + + if (response != null && response['results'] is List) { + List results = List.from(response['results']); + final List filteredResults = []; + final Set seenPlaces = {}; + + for (final p in results) { + final name = p['name_ar'] ?? p['name'] ?? ''; + final district = p['district'] ?? ''; + final plat = p['latitude']?.toString() ?? '0'; + final plng = p['longitude']?.toString() ?? '0'; + + final dedupeKey = + "${name.trim().toLowerCase()}_${district.trim().toLowerCase()}"; + + if (!seenPlaces.contains(dedupeKey)) { + seenPlaces.add(dedupeKey); + + p['distanceKm'] = (p['distance'] as num).toDouble() / 1000.0; + p['latitude'] = plat; + p['longitude'] = plng; + p['name'] = name; + p['address'] = p['full_address'] ?? + (district.isNotEmpty + ? "$district، ${p['governorate'] ?? ''}" + : (p['governorate'] ?? '')); + + filteredResults.add(p); + } + } + + placesDestination = filteredResults; + update(); + } + } catch (e) { + Log.print('Exception in getPlaces: $e'); + } + } + + Future getPlacesStart() async { + final q = placeStartController.text.trim(); + if (q.isEmpty || q.length < 3) { + placesStart = []; + update(); + return; + } + + final lat = passengerLocation.latitude; + final lng = passengerLocation.longitude; + final country = CountryPolygons.getCountryName(passengerLocation); + + try { + final url = + '${AppLink.searchGeocoding}?q=${Uri.encodeComponent(q)}&lat=$lat&lng=$lng&radius=15000&country=$country'; + final response = await CRUD().getMapSaas(link: url); + + if (response != null && response['results'] is List) { + List list = List.from(response['results']); + for (final p in list) { + p['distanceKm'] = (p['distance'] as num).toDouble() / 1000.0; + p['latitude'] = p['latitude'].toString(); + p['longitude'] = p['longitude'].toString(); + p['name'] = p['name_ar'] ?? p['name'] ?? ''; + p['address'] = p['full_address'] ?? + (p['district'] != null + ? "${p['district']}، ${p['governorate'] ?? ''}" + : (p['governorate'] ?? '')); + } + placesStart = list; + update(); + } + } catch (e) { + Log.print('Exception in getPlacesStart: $e'); + } + } + + Future getPlacesListsWayPoint(int index) async { + final q = wayPoint0Controller.text.trim(); + if (q.length < 3) return; + + final lat = passengerLocation.latitude; + final lng = passengerLocation.longitude; + final country = CountryPolygons.getCountryName(passengerLocation); + + try { + final url = + '${AppLink.searchGeocoding}?q=${Uri.encodeComponent(q)}&lat=$lat&lng=$lng&radius=15000&country=$country'; + final response = await CRUD().getMapSaas(link: url); + + if (response != null && response['results'] is List) { + List list = List.from(response['results']); + for (final p in list) { + p['distanceKm'] = (p['distance'] as num).toDouble() / 1000.0; + p['latitude'] = p['latitude'].toString(); + p['longitude'] = p['longitude'].toString(); + p['name'] = p['name_ar'] ?? p['name'] ?? ''; + p['address'] = p['full_address'] ?? + (p['district'] != null + ? "${p['district']}، ${p['governorate'] ?? ''}" + : (p['governorate'] ?? '')); + } + wayPoint0 = list; + placeListResponseAll[index] = list; + update(); + } + } catch (e) { + Log.print('Error fetching places in WayPoint: $e'); + } + } + + Future getReverseGeocoding(LatLng location) async { + final lat = location.latitude; + final lng = location.longitude; + final url = '${AppLink.reverseGeocoding}?lat=$lat&lng=$lng'; + + try { + final response = await CRUD().getMapSaas(link: url); + + if (response != null && response is List && response.isNotEmpty) { + final data = response[0]; + String name = data['name_ar'] ?? data['name'] ?? 'Unknown Location'.tr; + return name; + } + return 'Unknown Location'.tr; + } catch (e) { + Log.print('ReverseGeocoding Exception: $e'); + return 'Unknown Location'.tr; + } + } + + void updateCurrentLocationFromCamera(LatLng target) { + Log.print('📍 updateCurrentLocationFromCamera: $target'); + newMyLocation = target; + + if (startLocationFromMap == true) { + Log.print('📍 Updating startLocationFromMap to $target'); + newStartPointLocation = target; + } else if (passengerStartLocationFromMap == true) { + Log.print('📍 Updating passengerStartLocationFromMap to $target'); + newStartPointLocation = target; + } + + int waypointsLength = Get.find().wayPoints.length; + if (waypointsLength > 0 && + wayPointIndex >= 0 && + wayPointIndex < placesCoordinate.length) { + Log.print('📍 Updating wayPointIndex $wayPointIndex to $target'); + placesCoordinate[wayPointIndex] = + '${target.latitude},${target.longitude}'; + } + if (Get.isRegistered()) { + final rideLifecycle = Get.find(); + rideLifecycle.getLocationArea(target.latitude, target.longitude); + rideLifecycle.resetNoRideSearch(); + } + update(); + } + + void onCameraMoveThrottled(CameraPosition pos) { + _camThrottle?.cancel(); + _camThrottle = Timer(const Duration(milliseconds: 160), () { + Log.print('📸 onCameraMoveThrottled: ${pos.target}'); + int waypointsLength = Get.find().wayPoints.length; + int index = wayPointIndex; + if (waypointsLength > 0 && index < placesCoordinate.length) { + placesCoordinate[index] = + '${pos.target.latitude},${pos.target.longitude}'; + } + newMyLocation = pos.target; + }); + } + + void convertHintTextPlaces(int index, var res) { + if (placeListResponseAll[index].isEmpty) { + placeListResponseAll[index] = res; + hintTextwayPointStringAll[index] = 'Search for your Start point'.tr; + update(); + } else { + hintTextwayPointStringAll[index] = res['name']; + currentLocationStringAll[index] = res['name']; + placesCoordinate[index] = + '${res['geometry']['location']['lat']},${res['geometry']['location']['lng']}'; + placeListResponseAll[index] = []; + allTextEditingPlaces[index].clear(); + update(); + Get.back(); + } + } + + void convertHintTextDestinationNewPlaces(int index) { + if (placesDestination.isEmpty) { + hintTextDestinationPoint = 'Search for your destination'.tr; + update(); + } else { + var res = placesDestination[index]; + hintTextDestinationPoint = res['displayName']?['text'] ?? + res['formattedAddress'] ?? + res['name'] ?? + 'Unknown Place'; + double? lat = res['location']?['latitude'] ?? + double.tryParse(res['latitude']?.toString() ?? ''); + double? lng = res['location']?['longitude'] ?? + double.tryParse(res['longitude']?.toString() ?? ''); + + if (lat != null && lng != null) { + newMyLocation = LatLng(lat, lng); + final mapEngine = Get.find(); + mapEngine.mapController + ?.animateCamera(CameraUpdate.newLatLngZoom(newMyLocation, 16)); + } + update(); + } + } + + void convertHintTextDestinationNewPlacesFromRecent( + List recentLocations, int index) { + hintTextDestinationPoint = recentLocations[index]['name']; + double lat = recentLocations[index]['latitude']; + double lng = recentLocations[index]['longitude']; + newMyLocation = LatLng(lat, lng); + + final mapEngine = Get.find(); + mapEngine.mapController + ?.animateCamera(CameraUpdate.newLatLngZoom(newMyLocation, 16)); + update(); + } + + void clearPlacesDestination() { + placesDestination = []; + hintTextDestinationPoint = 'Search for your destination'.tr; + update(); + } + + void clearPlacesStart() { + placesStart = []; + hintTextStartPoint = 'Search for your Start point'.tr; + update(); + } + + void clearPlaces(int index) { + placeListResponseAll[index] = []; + hintTextwayPointStringAll[index] = 'Search for waypoint'.tr; + update(); + } + + Future?> extractCoordinatesFromLinkAsync( + String link) async { + try { + if (link.startsWith('geo:') || link.startsWith('google.navigation:')) { + RegExp regex = RegExp(r'(-?\d+\.\d+)[,/~=](-?\d+\.\d+)'); + var match = regex.firstMatch(link); + if (match != null) { + double lat = double.parse(match.group(1)!); + double lng = double.parse(match.group(2)!); + if (lat > 40 && lat > lng) { + double temp = lat; + lat = lng; + lng = temp; + } + return {'latitude': lat, 'longitude': lng}; + } + } + + int urlStartIndex = link.indexOf(RegExp(r'https?://')); + if (urlStartIndex == -1) return null; + String cleanLink = link.substring(urlStartIndex).trim(); + + Uri uri = Uri.parse(cleanLink); + String finalUrl = cleanLink; + + if (cleanLink.contains('goo.gl') || + cleanLink.contains('maps.google.com')) { + try { + var response = + await http.get(uri).timeout(const Duration(seconds: 5)); + finalUrl = response.request?.url.toString() ?? cleanLink; + } catch (e) { + Log.print('Redirect logic failed, using original: $e'); + } + } + + RegExp regex = RegExp(r'(-?\d+\.\d+)[,/~](-?\d+\.\d+)'); + var match = regex.firstMatch(finalUrl); + + if (match != null) { + double lat = double.parse(match.group(1)!); + double lng = double.parse(match.group(2)!); + + if (lat > 40 && lat > lng) { + Log.print("⚠️ Detected Swapped Coordinates in Link. Correcting..."); + double temp = lat; + lat = lng; + lng = temp; + } + + return { + 'latitude': lat, + 'longitude': lng, + }; + } + } catch (e) { + Log.print('Error parsing location link: $e'); + } + return null; + } + + void handleWhatsAppLink(String link) async { + Map? coordinates = + await extractCoordinatesFromLinkAsync(link); + + if (coordinates != null) { + latitudeWhatsApp = coordinates['latitude']!; + longitudeWhatsApp = coordinates['longitude']!; + Log.print( + 'Extracted coordinates: Lat: $latitudeWhatsApp, Long: $longitudeWhatsApp'); + } else { + Log.print('Failed to extract coordinates from the link'); + } + } + + void goToWhatappLocation() async { + if (sosFormKey.currentState!.validate()) { + Map? coordinates = + await extractCoordinatesFromLinkAsync(whatsAppLocationText.text); + + if (coordinates != null) { + latitudeWhatsApp = coordinates['latitude']!; + longitudeWhatsApp = coordinates['longitude']!; + + Log.print( + '📍 Final Coordinates for OSM: Lat: $latitudeWhatsApp, Lng: $longitudeWhatsApp'); + + final mapEngine = Get.find(); + mapEngine.changeIsWhatsAppOrder(true); + Get.back(); + + myDestination = LatLng(latitudeWhatsApp, longitudeWhatsApp); + + if (passengerLocation != null) { + await mapEngine.mapController?.animateCamera(CameraUpdate.newLatLng( + LatLng(passengerLocation.latitude, passengerLocation.longitude))); + } + + mapEngine.changeMainBottomMenuMap(); + passengerStartLocationFromMap = true; + mapEngine.isPickerShown = true; + update(); + } else { + mySnackbarWarning('لم نتمكن من استخراج الموقع من الرابط'); + } + } + } + + void addMenuWaypoint() { + if (activeMenuWaypointCount >= 2) return; + activeMenuWaypointCount++; + final mapEngine = Get.find(); + mapEngine.mainBottomMenuMapHeight = + Get.height * .6 + (activeMenuWaypointCount * 56); + update(); + } + + void removeMenuWaypoint(int index) { + if (index < 0 || index >= 2) return; + if (index == 0 && activeMenuWaypointCount == 2) { + menuWaypoints[0] = menuWaypoints[1]; + menuWaypointNames[0] = menuWaypointNames[1]; + } + menuWaypoints[activeMenuWaypointCount - 1] = null; + menuWaypointNames[activeMenuWaypointCount - 1] = ''; + activeMenuWaypointCount--; + final mapEngine = Get.find(); + mapEngine.mainBottomMenuMapHeight = + Get.height * .6 + (activeMenuWaypointCount * 56); + update(); + } + + void clearAllMenuWaypoints() { + menuWaypoints = [null, null]; + menuWaypointNames = ['', '']; + activeMenuWaypointCount = 0; + isPickingWaypoint = false; + pickingWaypointIndex = -1; + update(); + } + + void startPickingWaypointOnMap(int index) { + pickingWaypointIndex = index; + isPickingWaypoint = true; + final mapEngine = Get.find(); + mapEngine.isPickerShown = true; + mapEngine.heightPickerContainer = 150; + mapEngine.isMainBottomMenuMap = true; + mapEngine.mainBottomMenuMapHeight = Get.height * .22; + update(); + } + + void setMenuWaypointFromMap(int index, LatLng position) { + Log.print('📍 setMenuWaypointFromMap called: index=$index, pos=$position'); + if (index < 0 || index >= 2) return; + menuWaypoints[index] = position; + menuWaypointNames[index] = + '${position.latitude.toStringAsFixed(4)}, ${position.longitude.toStringAsFixed(4)}'; + isPickingWaypoint = false; + pickingWaypointIndex = -1; + final mapEngine = Get.find(); + mapEngine.isPickerShown = false; + mapEngine.isMainBottomMenuMap = false; + mapEngine.mainBottomMenuMapHeight = + Get.height * .6 + (activeMenuWaypointCount * 56); + update(); + } + + void setMenuWaypointFromSearch(int index, LatLng pos, String name) { + if (index < 0 || index >= 2) return; + menuWaypoints[index] = pos; + menuWaypointNames[index] = name; + update(); + } + + String buildOsrmWaypointCoords() { + String coords = ''; + for (int i = 0; i < activeMenuWaypointCount; i++) { + final wp = menuWaypoints[i]; + if (wp != null) { + coords += ';${wp.longitude},${wp.latitude}'; + } + } + return coords; + } + + void changeHeightPointsPageForRider() { + final mapEngine = Get.find(); + mapEngine.isPointsPageForRider = !mapEngine.isPointsPageForRider; + mapEngine.heightPointsPageForRider = + mapEngine.isPointsPageForRider == true ? Get.height : 0; + update(); + } + + getCoordinateFromMapWayPoints(int index) { + placesCoordinate[index] = newStartPointLocation.toString(); + update(); + } + + void addWaypoint(Map placeDetails) { + waypoints.add(placeDetails); + update(); + } + + void removeWaypoint(int index) { + if (index >= 0 && index < waypoints.length) { + waypoints.removeAt(index); + update(); + } + } + + getFavioratePlaces() async { + recentPlaces = await sql.getCustomQuery( + 'SELECT * FROM ${TableName.recentLocations} ORDER BY createdAt DESC'); + update(); + } + + void _listenForDeepLink() { + ever(_deepLinkController.rawDeepLink, (String? link) async { + if (link != null && link.isNotEmpty) { + Log.print('📍 MapPassengerController processing link: $link'); + + Map? coordinates = + await extractCoordinatesFromLinkAsync(link); + + if (coordinates != null) { + double destLat = coordinates['latitude']!; + double destLng = coordinates['longitude']!; + myDestination = LatLng(destLat, destLng); + + if (passengerLocation == null || + (passengerLocation.latitude == 0 && + passengerLocation.longitude == 0)) { + Log.print('⏳ Waiting for current location to calculate route...'); + await getLocation(); + } + + if (passengerLocation != null) { + String originStr = + '${passengerLocation.latitude},${passengerLocation.longitude}'; + String destStr = '$destLat,$destLng'; + + Log.print( + '🚀 Drawing route from Deep Link: $originStr to $destStr'); + + final mapEngine = Get.find(); + mapEngine.clearPolyline(); + waypoints.clear(); + clearAllMenuWaypoints(); + + final rideLife = Get.find(); + await rideLife.getDirectionMap(originStr, destStr); + + mapEngine.isBottomSheetShown = true; + mapEngine.heightBottomSheetShown = 250; + update(); + + Get.snackbar( + 'Location Received'.tr, + 'Route and prices have been calculated successfully!'.tr, + backgroundColor: AppColor.greenColor, + colorText: Colors.white, + ); + } + } else { + Log.print('⚠️ Could not extract valid coordinates from link: $link'); + } + + _deepLinkController.rawDeepLink.value = null; + } + }); + + if (_deepLinkController.rawDeepLink.value != null && + _deepLinkController.rawDeepLink.value!.isNotEmpty) { + String link = _deepLinkController.rawDeepLink.value!; + _deepLinkController.rawDeepLink.value = null; + + Future.delayed(const Duration(milliseconds: 500), () async { + Log.print( + '📍 MapPassengerController processing link (Cold Start): $link'); + + Map? coordinates = + await extractCoordinatesFromLinkAsync(link); + + if (coordinates != null) { + double destLat = coordinates['latitude']!; + double destLng = coordinates['longitude']!; + myDestination = LatLng(destLat, destLng); + + if (passengerLocation == null || + (passengerLocation.latitude == 0 && + passengerLocation.longitude == 0)) { + await getLocation(); + } + + if (passengerLocation != null) { + String originStr = + '${passengerLocation.latitude},${passengerLocation.longitude}'; + String destStr = '$destLat,$destLng'; + + final mapEngine = Get.find(); + mapEngine.clearPolyline(); + waypoints.clear(); + clearAllMenuWaypoints(); + + final rideLife = Get.find(); + await rideLife.getDirectionMap(originStr, destStr); + + mapEngine.isBottomSheetShown = true; + mapEngine.heightBottomSheetShown = 250; + update(); + } + } + }); + } + } + + // --- Polygon Math and University Check Methods --- + bool isPointInPolygon(LatLng point, List polygon) { + int intersections = 0; + for (int i = 0; i < polygon.length; i++) { + LatLng vertex1 = polygon[i]; + LatLng vertex2 = polygon[(i + 1) % polygon.length]; + + if (_rayIntersectsSegment(point, vertex1, vertex2)) { + intersections++; + } + } + return intersections % 2 != 0; + } + + bool _rayIntersectsSegment(LatLng point, LatLng vertex1, LatLng vertex2) { + double px = point.longitude; + double py = point.latitude; + + double v1x = vertex1.longitude; + double v1y = vertex1.latitude; + double v2x = vertex2.longitude; + double v2y = vertex2.latitude; + + if ((py < v1y && py < v2y) || (py > v1y && py > v2y)) { + return false; + } + + double intersectX = v1x + (py - v1y) * (v2x - v1x) / (v2y - v1y); + return intersectX > px; + } + + String checkPassengerLocation(LatLng passengerLocation, + List> universityPolygons, List universityNames) { + for (int i = 0; i < universityPolygons.length; i++) { + if (isPointInPolygon(passengerLocation, universityPolygons[i])) { + isInUniversity = true; + return "Passenger is in ${universityNames[i]}"; + } + } + return "Passenger is not in any university"; + } + + void getPassengerLocationUniversity() { + passengerLocationStringUnvirsity = checkPassengerLocation( + passengerLocation, + UniversitiesPolygons.universityPolygons, + UniversitiesPolygons.universityNames, + ); + Log.print(passengerLocationStringUnvirsity); + } + + void getMapPointsForAllMethods() async { + final mapEngine = Get.find(); + final rideLife = Get.find(); + + mapEngine.clearPolyline(); + mapEngine.isMarkersShown = false; + mapEngine.isWayPointStopsSheetUtilGetMap = false; + mapEngine.isWayPointSheet = false; + rideLife.durationToRide = 0; + rideLife.distanceOfDestination = 0; + mapEngine.wayPointSheetHeight = 0; + rideLife.remainingTime = 25; + rideLife.haveSteps = true; + + // Filter out empty value + coordinatesWithoutEmpty = + placesCoordinate.where((coord) => coord.isNotEmpty).toList(); + latestPosition = LatLng( + double.parse(coordinatesWithoutEmpty.last.split(',')[0]), + double.parse(coordinatesWithoutEmpty.last.split(',')[1])); + for (var i = 0; i < coordinatesWithoutEmpty.length; i++) { + if ((i + 1) < coordinatesWithoutEmpty.length) { + await rideLife.getMapPoints( + coordinatesWithoutEmpty[i].toString(), + coordinatesWithoutEmpty[i + 1].toString(), + i, + ); + if (i == 0) { + startNameAddress = rideLife.data[0]['start_address']; + } + if (i == coordinatesWithoutEmpty.length - 1) { + endNameAddress = rideLife.data[0]['end_address']; + } + } + } + + if (rideLife.haveSteps) { + String latestWaypoint = + placesCoordinate.lastWhere((coord) => coord.isNotEmpty); + update(); + } + } + + double _haversineKm(double lat1, double lon1, double lat2, double lon2) { + const double r = 6371.0; + double dLat = (lat2 - lat1) * pi / 180.0; + double dLon = (lon2 - lon1) * pi / 180.0; + double a = sin(dLat / 2) * sin(dLat / 2) + + cos(lat1 * pi / 180.0) * + cos(lat2 * pi / 180.0) * + sin(dLon / 2) * + sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + return r * c; + } + + double _kmToLatDelta(double km) { + return km / 111.0; + } + + double _kmToLngDelta(double km, double lat) { + return km / (111.0 * cos(lat * pi / 180.0)); + } + + String _buildOsrmWaypointCoords() { + String coords = ''; + for (int i = 0; i < activeMenuWaypointCount; i++) { + final wp = menuWaypoints[i]; + if (wp != null) { + coords += ';${wp.longitude},${wp.latitude}'; + } + } + return coords; + } + + @override + void onClose() { + _camThrottle?.cancel(); + super.onClose(); + } +} diff --git a/lib/controller/home/map/map_engine_controller.dart b/lib/controller/home/map/map_engine_controller.dart new file mode 100644 index 0000000..43c7aa7 --- /dev/null +++ b/lib/controller/home/map/map_engine_controller.dart @@ -0,0 +1,809 @@ +import 'dart:async'; +import 'dart:math' show cos, max, min, pi, pow, sqrt; +import 'dart:typed_data'; +import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import 'package:image/image.dart' as img; + +import '../../../constant/colors.dart'; +// contains global 'box' +import '../../../print.dart'; +import '../../../views/home/map_widget.dart/cancel_raide_page.dart'; +import 'location_search_controller.dart'; +import 'nearby_drivers_controller.dart'; +import 'ride_lifecycle_controller.dart'; +import '../points_for_rider_controller.dart'; +import '../../../constant/univeries_polygon.dart'; + +class MapEngineController extends GetxController { + IntaleqMapController? mapController; + bool isStyleLoaded = false; + bool isIconsLoaded = false; + + Set markers = {}; + Set polyLines = {}; + List polylineCoordinates = []; + Set polygons = {}; + Set circles = {}; + + LatLngBounds? lastComputedBounds; + bool mapType = false; + bool mapTrafficON = false; + bool isMarkersShown = false; + + String markerIcon = "marker_icon"; + String tripIcon = "trip_icon"; + String startIcon = "start_icon"; + String endIcon = "end_icon"; + String carIcon = "car_icon"; + String motoIcon = "moto_icon"; + String ladyIcon = "lady_icon"; + + double height = 150; + double heightMenu = 0; + double widthMenu = 0; + double heightPickerContainer = 90; + double heightPointsPageForRider = 0; + double mainBottomMenuMapHeight = Get.height * .2; + double wayPointSheetHeight = 0; + bool heightMenuBool = false; + bool isPickerShown = false; + bool isPointsPageForRider = false; + bool isBottomSheetShown = false; + bool reloadStartApp = false; + bool isCancelRidePageShown = false; + bool isCashConfirmPageShown = false; + bool isPaymentMethodPageShown = false; + bool isRideFinished = false; + bool rideConfirm = false; + bool isMainBottomMenuMap = true; + + bool isWayPointSheet = false; + bool isWayPointStopsSheet = false; + bool isWayPointStopsSheetUtilGetMap = false; + double heightBottomSheetShown = 0; + double cashConfirmPageShown = 250; + double widthMapTypeAndTraffic = 50; + double paymentPageShown = Get.height * .6; + + bool isAnotherOreder = false; + bool isWhatsAppOrder = false; + + Map _animationTimers = {}; + final int updateIntervalMs = 100; + final double minMovementThreshold = 1.0; + + void onMapCreated(IntaleqMapController controller) { + mapController = controller; + update(); + } + + void onStyleLoaded() async { + Log.print('🗺️ Intaleq Map Style Loaded. Initializing...'); + isStyleLoaded = true; + await _loadMapIcons(); + + final locationSearch = Get.find(); + Get.find().reinit(); + + if (mapController != null) { + if (markers.isNotEmpty && lastComputedBounds != null) { + await _safeAnimateCameraBounds(lastComputedBounds); + } else { + mapController!.animateCamera( + CameraUpdate.newLatLng(locationSearch.passengerLocation), + ); + } + } + update(); + } + + Future _safeAnimateCameraBounds(LatLngBounds? bounds, + {double left = 60, + double top = 60, + double right = 60, + double bottom = 60}) async { + if (bounds == null || mapController == null) return; + + try { + if (bounds.northeast.latitude == bounds.southwest.latitude && + bounds.northeast.longitude == bounds.southwest.longitude) { + Log.print( + '⚠️ _safeAnimateCameraBounds: Bounds are a single point, zooming to point instead.'); + await mapController + ?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 15)); + return; + } + + await Future.delayed(const Duration(milliseconds: 200)); + + await mapController?.animateCamera( + CameraUpdate.newLatLngBounds( + bounds, + left: left, + top: top, + right: right, + bottom: bottom, + ), + ); + } catch (e) { + Log.print('❌ _safeAnimateCameraBounds CRASH PREVENTED: $e'); + try { + await mapController + ?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 14)); + } catch (_) {} + } + } + + Future _loadMapIcons() async { + isIconsLoaded = false; + for (int i = 0; i < 15; i++) { + if (mapController != null && isStyleLoaded) break; + await Future.delayed(const Duration(milliseconds: 200)); + } + + if (mapController == null || !isStyleLoaded) { + Log.print( + '⚠️ _loadMapIcons: mapController or style not ready. Icons may not load.'); + } + + await _addMapImage(startIcon, 'assets/images/A.png'); + await _addMapImage(endIcon, 'assets/images/b.png'); + await _addMapImage(carIcon, 'assets/images/car.png'); + await _addMapImage(motoIcon, 'assets/images/moto.png'); + await _addMapImage(ladyIcon, 'assets/images/lady.png'); + await _addMapImage('picker_icon', 'assets/images/picker.png'); + await _addMapImage('orange_marker', 'assets/images/moto1.png'); + await _addMapImage('violet_marker', 'assets/images/lady1.png'); + + isIconsLoaded = true; + markers = markers.map((m) => m.copyWith()).toSet(); + update(); + + if (Get.isRegistered()) { + Get.find() + .getCarsLocationByPassengerAndReloadMarker(); + } + } + + Future _addMapImage(String id, String path) async { + try { + final ByteData bytes = await rootBundle.load(path); + final size = _getImageSize(id); + if (size != null && (id == carIcon || id == motoIcon || id == ladyIcon)) { + final resized = await _resizeImage(bytes.buffer.asUint8List(), size); + await mapController?.addImage(id, resized); + Log.print( + 'delimited: successfully added resized map image: $id (${size}x${size})'); + } else { + await mapController?.addImage(id, bytes.buffer.asUint8List()); + Log.print('delimited: successfully added map image: $id'); + } + } catch (e) { + Log.print('❌ Error loading map icon $id: $e'); + } + } + + int? _getImageSize(String id) { + if (id == carIcon || id == motoIcon || id == ladyIcon) return 120; + return null; + } + + Future _resizeImage(Uint8List bytes, int size) async { + return await compute((Uint8List data) { + final image = img.decodeImage(data); + if (image == null) return data; + final resized = img.copyResize(image, width: size, height: size); + return Uint8List.fromList(img.encodePng(resized)); + }, bytes); + } + + void clearPolyline() { + polyLines.clear(); + update(); + } + + LatLngBounds calculateBounds(double lat, double lng, double radiusInMeters) { + const double earthRadius = 6378137.0; + + double latDelta = (radiusInMeters / earthRadius) * (180 / pi); + double lngDelta = + (radiusInMeters / (earthRadius * cos(pi * lat / 180))) * (180 / pi); + + double minLat = lat - latDelta; + double maxLat = lat + latDelta; + + double minLng = lng - lngDelta; + double maxLng = lng + lngDelta; + + minLat = max(-90.0, minLat); + maxLat = min(90.0, maxLat); + + minLng = (minLng + 180) % 360 - 180; + maxLng = (maxLng + 180) % 360 - 180; + + if (minLng > maxLng) { + double temp = minLng; + minLng = maxLng; + maxLng = temp; + } + + return LatLngBounds( + southwest: LatLng(minLat, minLng), + northeast: LatLng(maxLat, maxLng), + ); + } + + Future playRouteAnimation( + List coords, LatLngBounds? bounds) async { + const List segmentColors = [ + Color(0xFF109642), // Green + Color(0xFFF59E0B), // Amber + Color(0xFF7C3AED), // Purple + Color(0xFFEF4444), // Red + ]; + + Set newPolylines = {}; + final locationSearch = Get.find(); + + if (locationSearch.activeMenuWaypointCount > 0) { + List splitIndices = []; + for (int w = 0; w < locationSearch.activeMenuWaypointCount; w++) { + final wp = locationSearch.menuWaypoints[w]; + if (wp == null) continue; + int bestIdx = 0; + double bestDist = double.infinity; + for (int j = 0; j < coords.length; j++) { + final dx = coords[j].latitude - wp.latitude; + final dy = coords[j].longitude - wp.longitude; + final d = dx * dx + dy * dy; + if (d < bestDist) { + bestDist = d; + bestIdx = j; + } + } + splitIndices.add(bestIdx); + } + splitIndices.sort(); + + List boundaries = [0, ...splitIndices, coords.length - 1]; + for (int s = 0; s < boundaries.length - 1; s++) { + int from = boundaries[s]; + int to = boundaries[s + 1] + 1; + if (to > coords.length) to = coords.length; + if (from >= to - 1) continue; + final segCoords = coords.sublist(from, to); + if (segCoords.length < 2) continue; + final color = segmentColors[s % segmentColors.length]; + + newPolylines.add(Polyline( + polylineId: PolylineId('segment_$s'), + points: segCoords, + color: color, + width: 6, + )); + } + } else { + newPolylines.add(Polyline( + polylineId: const PolylineId('route_primary'), + points: coords, + color: AppColor.primaryColor, + width: 6, + )); + } + + polyLines = newPolylines; + update(); + + Log.print( + '🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map'); + + if (bounds != null) { + await _safeAnimateCameraBounds(bounds); + } + } + + void _fitCameraToPoints(LatLng p1, LatLng p2) async { + if (mapController == null) return; + + if (p1.latitude == p2.latitude && p1.longitude == p2.longitude) { + try { + mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 17)); + } catch (e) { + Log.print("Error animating to single point: $e"); + } + return; + } + + double minLat = min(p1.latitude, p2.latitude); + double maxLat = max(p1.latitude, p2.latitude); + double minLng = min(p1.longitude, p2.longitude); + double maxLng = max(p1.longitude, p2.longitude); + + if ((maxLat - minLat).abs() < 0.002 && (maxLng - minLng).abs() < 0.002) { + try { + mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 16)); + } catch (e) { + Log.print("Error animating to single point: $e"); + } + return; + } + + double padding = 50.0; + + try { + await mapController?.animateCamera( + CameraUpdate.newLatLngBounds( + LatLngBounds( + southwest: LatLng(minLat, minLng), + northeast: LatLng(maxLat, maxLng), + ), + left: padding, + top: padding, + right: padding, + bottom: padding, + ), + ); + } catch (e) { + Log.print("Error animating bounds: $e"); + try { + LatLng center = LatLng((minLat + maxLat) / 2, (minLng + maxLng) / 2); + mapController?.animateCamera(CameraUpdate.newLatLngZoom(center, 14)); + } catch (_) {} + } + } + + void fitCameraToPoints(LatLng p1, LatLng p2) { + _fitCameraToPoints(p1, p2); + } + + void clearMarkersExceptStartEndAndDriver() { + const String currentDriverMarkerId = 'assigned_driver_marker'; + markers.removeWhere((marker) { + String id = marker.markerId.value; + if (id == 'start') return false; + if (id == 'end') return false; + if (id == currentDriverMarkerId) return false; + return true; + }); + update(); + } + + void clearMarkersExceptStartEnd() { + markers.removeWhere((marker) { + String id = marker.markerId.value; + return id != 'start' && id != 'end'; + }); + update(); + } + + void _updateMarkerPosition( + LatLng newPosition, double newHeading, String icon) { + const String markerId = 'driverToPassengers'; + final mId = MarkerId(markerId); + final existingMarker = markers.cast().firstWhere( + (m) => m?.markerId == mId, + orElse: () => null, + ); + + if (existingMarker != null) { + _smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon); + } else { + markers = { + ...markers, + Marker( + markerId: mId, + position: newPosition, + rotation: newHeading, + icon: InlqBitmap.fromStyleImage(icon), + anchor: const Offset(0.5, 0.5), + ), + }; + update(); + } + mapController?.animateCamera(CameraUpdate.newLatLng(newPosition)); + } + + void updateMarkerPosition( + LatLng newPosition, double newHeading, String icon) { + _updateMarkerPosition(newPosition, newHeading, icon); + } + + void _smoothlyUpdateMarker( + Marker oldMarker, LatLng newPosition, double newHeading, String icon) { + double distance = Geolocator.distanceBetween( + oldMarker.position.latitude, + oldMarker.position.longitude, + newPosition.latitude, + newPosition.longitude); + + if (distance < 2.0) return; + + final MarkerId markerIdKey = oldMarker.markerId; + _animationTimers[markerIdKey.value]?.cancel(); + + int ticks = 0; + const int totalSteps = 20; + const int stepDuration = 50; + + double latStep = + (newPosition.latitude - oldMarker.position.latitude) / totalSteps; + double lngStep = + (newPosition.longitude - oldMarker.position.longitude) / totalSteps; + double headingStep = (newHeading - oldMarker.rotation) / totalSteps; + + LatLng currentPos = oldMarker.position; + double currentHeading = oldMarker.rotation; + + _animationTimers[markerIdKey.value] = + Timer.periodic(const Duration(milliseconds: stepDuration), (timer) { + ticks++; + + currentPos = + LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep); + currentHeading += headingStep; + + final updatedMarker = oldMarker.copyWith( + position: currentPos, + rotation: currentHeading, + icon: InlqBitmap.fromStyleImage(icon), + ); + + markers = { + ...markers.where((m) => m.markerId != markerIdKey), + updatedMarker, + }; + + if (mapController != null) { + mapController!.animateCamera(CameraUpdate.newLatLng(currentPos)); + } + + update(); + + if (ticks >= totalSteps) { + timer.cancel(); + _animationTimers.remove(markerIdKey.value); + } + }); + } + + // تحديث موقع العلامة (Marker) واتجاهها بسلاسة على الخريطة. + // تحسب الدالة المسافة بين الموقع الحالي والجديد؛ وإذا كانت أكبر من مترين، + // تقوم بتقسيم الحركة والدوران إلى 20 خطوة متباعدة بـ 50 مللي ثانية (إجمالي ثانية واحدة). + // يتم تحديث موضع العلامة وتحريك الكاميرا تدريجياً لتبدو حركة السيارة انسيابية. + void smoothlyUpdateMarker( + Marker oldMarker, LatLng newPosition, double newHeading, String icon) { + _smoothlyUpdateMarker(oldMarker, newPosition, newHeading, icon); + } + + void changeBottomSheetShown({bool? forceValue}) { + if (forceValue != null) { + isBottomSheetShown = forceValue; + } else { + isBottomSheetShown = !isBottomSheetShown; + } + heightBottomSheetShown = isBottomSheetShown == true ? 250 : 0; + update(); + } + + void changeCashConfirmPageShown() { + isCashConfirmPageShown = !isCashConfirmPageShown; + final rideLife = Get.find(); + rideLife.isCashSelectedBeforeConfirmRide = true; + cashConfirmPageShown = isCashConfirmPageShown == true ? 250 : 0; + update(); + rideLife.update(); + } + + void changePaymentMethodPageShown() { + isPaymentMethodPageShown = !isPaymentMethodPageShown; + paymentPageShown = isPaymentMethodPageShown == true ? Get.height * .6 : 0; + update(); + } + + void changeMapType() { + mapType = !mapType; + update(); + } + + void changeMapTraffic() { + mapTrafficON = !mapTrafficON; + update(); + } + + void changeisAnotherOreder(bool val) { + isAnotherOreder = val; + update(); + } + + void changeIsWhatsAppOrder(bool val) { + isWhatsAppOrder = val; + update(); + } + + void changeCancelRidePageShow() { + showCancelRideBottomSheet(); + isCancelRidePageShown = !isCancelRidePageShown; + update(); + if (Get.isRegistered()) { + Get.find().update(); + } + } + + void getDrawerMenu() { + heightMenuBool = !heightMenuBool; + widthMapTypeAndTraffic = heightMenuBool == true ? 0 : 50; + heightMenu = heightMenuBool == true ? 80 : 0; + widthMenu = heightMenuBool == true ? 110 : 0; + update(); + } + + void changeMainBottomMenuMap() { + if (isWayPointStopsSheetUtilGetMap == true) { + changeWayPointSheet(); + } else { + isMainBottomMenuMap = !isMainBottomMenuMap; + mainBottomMenuMapHeight = + isMainBottomMenuMap == true ? Get.height * .22 : Get.height * .6; + isWayPointSheet = false; + if (heightMenuBool == true) { + getDrawerMenu(); + } + Get.find().initilizeGetStorage(); + update(); + } + } + + void downPoints() { + if (Get.find().wayPoints.length < 2) { + isWayPointStopsSheetUtilGetMap = false; + isWayPointSheet = false; + wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0; + update(); + } + update(); + } + + void changeWayPointSheet() { + isWayPointSheet = !isWayPointSheet; + wayPointSheetHeight = isWayPointSheet == false ? 0 : Get.height * .45; + update(); + } + + void changeWayPointStopsSheet() { + final locationSearch = Get.find(); + if (locationSearch.wayPointIndex > -1) { + isWayPointStopsSheet = true; + isWayPointStopsSheetUtilGetMap = true; + } + isWayPointStopsSheet = !isWayPointStopsSheet; + wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0; + update(); + } + + void changeHeightPlaces() { + final locationSearch = Get.find(); + if (locationSearch.placesDestination.isEmpty) { + height = 0; + update(); + } else { + height = 150; + update(); + } + } + + void changeHeightStartPlaces() { + final locationSearch = Get.find(); + if (locationSearch.placesStart.isEmpty) { + height = 0; + update(); + } else { + height = 150; + update(); + } + } + + void changeHeightPlacesAll(int index) { + final locationSearch = Get.find(); + if (locationSearch.placeListResponseAll[index].isEmpty) { + height = 0; + update(); + } else { + height = 150; + update(); + } + } + + void changeHeightPlaces1() { + final locationSearch = Get.find(); + if (locationSearch.wayPoint1.isEmpty) { + height = 0; + update(); + } else { + height = 150; + update(); + } + } + + void changeHeightPlaces2() { + final locationSearch = Get.find(); + if (locationSearch.wayPoint2.isEmpty) { + height = 0; + update(); + } else { + height = 150; + update(); + } + } + + void changeHeightPlaces3() { + final locationSearch = Get.find(); + if (locationSearch.wayPoint3.isEmpty) { + height = 0; + update(); + } else { + height = 150; + update(); + } + } + + void changeHeightPlaces4() { + final locationSearch = Get.find(); + if (locationSearch.wayPoint4.isEmpty) { + height = 0; + update(); + } else { + height = 150; + update(); + } + } + + void hidePlaces() { + height = 0; + update(); + } + + void changePickerShown() { + isPickerShown = !isPickerShown; + heightPickerContainer = isPickerShown == true ? 150 : 90; + update(); + } + + void _initializePolygons() { + List> universityPolygons = + UniversitiesPolygons.universityPolygons; + + for (int i = 0; i < universityPolygons.length; i++) { + Polygon polygon = Polygon( + polygonId: PolygonId('univ_$i'), + points: universityPolygons[i], + fillColor: Colors.blueAccent.withOpacity(0.2), + strokeColor: Colors.blueAccent, + strokeWidth: 2, + ); + polygons.add(polygon); + } + update(); + } + + void _applyLowEndModeIfNeeded() { + // Placeholder comment from original + } + + Future _initMinimalIcons() async { + // Icons are loaded dynamically + } + + Future _playRouteAnimation( + List coords, LatLngBounds? bounds) async { + const List segmentColors = [ + Color(0xFF109642), // Green + Color(0xFFF59E0B), // Amber + Color(0xFF7C3AED), // Purple + Color(0xFFEF4444), // Red + ]; + + Set newPolylines = {}; + final locSearch = Get.find(); + + if (locSearch.activeMenuWaypointCount > 0) { + List splitIndices = []; + for (int w = 0; w < locSearch.activeMenuWaypointCount; w++) { + final wp = locSearch.menuWaypoints[w]; + if (wp == null) continue; + int bestIdx = 0; + double bestDist = double.infinity; + for (int j = 0; j < coords.length; j++) { + final dx = coords[j].latitude - wp.latitude; + final dy = coords[j].longitude - wp.longitude; + final d = dx * dx + dy * dy; + if (d < bestDist) { + bestDist = d; + bestIdx = j; + } + } + splitIndices.add(bestIdx); + } + splitIndices.sort(); + + List boundaries = [0, ...splitIndices, coords.length - 1]; + for (int s = 0; s < boundaries.length - 1; s++) { + int from = boundaries[s]; + int to = boundaries[s + 1] + 1; + if (to > coords.length) to = coords.length; + if (from >= to - 1) continue; + final segCoords = coords.sublist(from, to); + if (segCoords.length < 2) continue; + final color = segmentColors[s % segmentColors.length]; + + newPolylines.add(Polyline( + polylineId: PolylineId('segment_$s'), + points: segCoords, + color: color, + width: 6, + )); + } + } else { + newPolylines.add(Polyline( + polylineId: const PolylineId('route_primary'), + points: coords, + color: AppColor.primaryColor, + width: 6, + )); + } + + polyLines = newPolylines; + update(); + + Log.print( + '🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map'); + + update(); + + if (bounds != null) { + await _safeAnimateCameraBounds(bounds); + } + } + + void reset() { + isPickerShown = false; + isPointsPageForRider = false; + isBottomSheetShown = false; + isCancelRidePageShown = false; + isCashConfirmPageShown = false; + isPaymentMethodPageShown = false; + isRideFinished = false; + rideConfirm = false; + isMainBottomMenuMap = true; + + isWayPointSheet = false; + isWayPointStopsSheet = false; + isWayPointStopsSheetUtilGetMap = false; + + heightBottomSheetShown = 0; + mainBottomMenuMapHeight = Get.height * 0.22; + wayPointSheetHeight = 0; + + markers.clear(); + polyLines.clear(); + polylineCoordinates.clear(); + + _animationTimers.forEach((key, timer) => timer.cancel()); + _animationTimers.clear(); + + update(); + } + + @override + void onClose() { + _animationTimers.forEach((key, timer) => timer.cancel()); + _animationTimers.clear(); + mapController = null; + super.onClose(); + } +} diff --git a/lib/controller/home/map/map_screen_binding.dart b/lib/controller/home/map/map_screen_binding.dart new file mode 100644 index 0000000..e24f441 --- /dev/null +++ b/lib/controller/home/map/map_screen_binding.dart @@ -0,0 +1,25 @@ +import 'package:get/get.dart'; + +import 'map_socket_controller.dart'; +import 'map_engine_controller.dart'; +import 'location_search_controller.dart'; +import 'nearby_drivers_controller.dart'; +import 'ride_lifecycle_controller.dart'; +import 'ui_interactions_controller.dart'; + +class MapScreenBinding extends Bindings { + @override + void dependencies() { + // 1. WebSocket Controller: Permanent and immediate + Get.put(MapSocketController()); + + // 2. Core Controllers (initialized when the screen opens or on demand) + Get.lazyPut(() => MapEngineController()); + Get.lazyPut(() => LocationSearchController()); + Get.lazyPut(() => NearbyDriversController()); + + // 3. Lifecycle and UI Interaction Controllers + Get.lazyPut(() => RideLifecycleController()); + Get.lazyPut(() => UiInteractionsController(), fenix: true); + } +} diff --git a/lib/controller/home/map/map_socket_controller.dart b/lib/controller/home/map/map_socket_controller.dart new file mode 100644 index 0000000..3d73bae --- /dev/null +++ b/lib/controller/home/map/map_socket_controller.dart @@ -0,0 +1,326 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:get/get.dart'; +import 'package:socket_io_client/socket_io_client.dart' as io_client; +import 'package:intaleq_maps/intaleq_maps.dart'; + +import '../../../constant/box_name.dart'; +import '../../../constant/links.dart'; +import '../../../main.dart'; // contains global 'box' +import '../../../print.dart'; +import 'ride_lifecycle_controller.dart'; +import 'nearby_drivers_controller.dart'; +import 'map_engine_controller.dart'; + +class MapSocketController extends GetxController { + late io_client.Socket socket; + bool isSocketConnected = false; + bool _isSocketInitialized = false; + Timer? _heartbeatTimer; + DateTime? _lastSocketLocationTime; + int _socketLocationUpdatesCount = 0; + Timer? _watchdogTimer; + + DateTime? get lastDriverLocationTime => _lastSocketLocationTime; + int get socketLocationUpdatesCount => _socketLocationUpdatesCount; + + void initConnectionWithSocket() { + if (isSocketConnected) return; + + String passengerId = box.read(BoxName.passengerID).toString(); + Log.print("🔌 Initializing Socket for Passenger: $passengerId"); + + socket = io_client.io( + AppLink.serverSocket, + io_client.OptionBuilder() + .setTransports(['websocket']) + .disableAutoConnect() + .setQuery({'id': passengerId}) + .setReconnectionAttempts(20) + .setReconnectionDelay(2000) + .setReconnectionDelayMax(10000) + .enableReconnection() + .setTimeout(20000) + .setExtraHeaders({'Connection': 'Upgrade'}) + .build(), + ); + _isSocketInitialized = true; + + socket.connect(); + + socket.onConnect((_) { + Log.print("✅ Socket Connected Successfully"); + isSocketConnected = true; + _startHeartbeat(); + + final rideLifecycle = Get.find(); + if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) { + socket.emit('subscribe_driver_location', { + 'ride_id': rideLifecycle.rideId, + 'driver_id': rideLifecycle.driverId, + }); + Log.print("📡 Re-subscribed to driver location after connect"); + } + update(); + }); + + socket.onDisconnect((_) { + Log.print("⚠️ Socket Disconnected — Auto-Reconnect will handle it"); + isSocketConnected = false; + + final rideLifecycle = Get.find(); + if (rideLifecycle.isActiveRideState()) { + Log.print("🔄 Enabling Fast Polling Fallback (4s) until reconnect..."); + rideLifecycle.startMasterTimerWithInterval(4); + } + update(); + }); + + socket.onReconnect((_) { + Log.print("🔁 Socket Reconnected Successfully!"); + isSocketConnected = true; + _startHeartbeat(); + + final rideLifecycle = Get.find(); + if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) { + socket.emit('subscribe_driver_location', { + 'ride_id': rideLifecycle.rideId, + 'driver_id': rideLifecycle.driverId, + }); + Log.print("📡 Re-subscribed to driver location after reconnect"); + } + + if (rideLifecycle.isActiveRideState()) { + Log.print("✅ Socket back online — stopping Fast Polling Fallback"); + rideLifecycle.cancelMasterTimer(); + } + update(); + }); + + socket.onReconnectAttempt((attemptNumber) { + Log.print("🔄 Socket Reconnect Attempt #$attemptNumber..."); + }); + + socket.onError((error) { + Log.print("❌ Socket Error: $error"); + isSocketConnected = false; + }); + + socket.on('connect_error', (error) { + Log.print("❌ Socket Connect Error: $error"); + isSocketConnected = false; + // في الإصدار 1.0.2 أحياناً auto-reconnect لا يعمل بعد connect_error + // نتأكد يدوياً من إعادة الاتصال + Future.delayed(const Duration(seconds: 3), () { + if (!isSocketConnected && _isSocketInitialized) { + Log.print("🔄 Manual reconnect after connect_error..."); + try { + socket.connect(); + } catch (e) { + Log.print("Manual reconnect error: $e"); + } + } + }); + }); + + socket.on('ride_status_change', (data) { + Log.print("📩 Socket Event: ride_status_change -> $data"); + _handleRideStatusChangeWithSocket(data); + }); + + socket.on('driver_location_update', (data) { + handleDriverLocationUpdate(data); + }); + } + + void _startHeartbeat() { + _heartbeatTimer?.cancel(); + _heartbeatTimer = Timer.periodic(const Duration(seconds: 15), (timer) { + if (isSocketConnected && socket.connected) { + socket.emit('heartbeat', + {'passenger_id': box.read(BoxName.passengerID).toString()}); + } + }); + } + + bool isSocketHealthy() { + if (!isSocketConnected) return false; + if (_lastSocketLocationTime == null) return false; + final diff = DateTime.now().difference(_lastSocketLocationTime!).inSeconds; + return diff < 20; + } + + void _handleRideStatusChangeWithSocket(dynamic data) { + if (data == null || data['status'] == null) return; + + String newStatus = data['status'].toString().toLowerCase(); + Log.print("🔔 Socket Status Update: $newStatus"); + + final rideLifecycle = Get.find(); + + Map? driverInfo; + if (data['driver_info'] != null && data['driver_info'] is Map) { + driverInfo = Map.from(data['driver_info']); + } + + switch (newStatus) { + case 'accepted': + case 'apply': + case 'applied': + rideLifecycle.processRideAcceptance( + driverData: driverInfo, source: "Socket"); + break; + + case 'arrived': + rideLifecycle.processDriverArrival("Socket"); + break; + + case 'started': + case 'begin': + rideLifecycle.processRideBegin(source: "Socket"); + break; + + case 'finished': + case 'ended': + _onRideFinishedWithSocket(data); + break; + + case 'cancelled': + rideLifecycle.processRideCancelledByDriver(data, source: "Socket"); + break; + + case 'no_drivers_found': + rideLifecycle.showNoDriverDialog(); + break; + } + } + + void _onRideFinishedWithSocket(dynamic data) { + Log.print("🏁 Ride Finished (Socket)"); + final rideLifecycle = Get.find(); + + var rawList = data['DriverList']; + List listToSend = []; + + if (rawList != null) { + if (rawList is List) { + listToSend = rawList; + } else if (rawList is String) { + try { + listToSend = jsonDecode(rawList); + } catch (e) { + Log.print("Error decoding DriverList: $e"); + } + } + } + + if (listToSend.isEmpty && data['price'] != null) { + listToSend = [ + rideLifecycle.driverId, + rideLifecycle.rideId, + rideLifecycle.driverToken, + data['price'].toString() + ]; + } + + rideLifecycle.processRideFinished(listToSend, source: "Socket"); + } + + void handleDriverLocationUpdate(dynamic data) { + if (!isSocketConnected || data == null) return; + _lastSocketLocationTime = DateTime.now(); + _socketLocationUpdatesCount++; + + final rideLifecycle = Get.find(); + if (rideLifecycle.driverId.isEmpty && + (data['driver_id'] ?? data['driverId']) != null) { + rideLifecycle.driverId = + (data['driver_id'] ?? data['driverId']).toString(); + } + + if (_socketLocationUpdatesCount >= 3 && + rideLifecycle.locationPollingTimer != null) { + Log.print("✅ Socket delivering locations reliably. Stopping polling."); + rideLifecycle.stopDriverLocationPolling(); + } + + try { + double lat = double.tryParse( + (data['latitude'] ?? data['lat'])?.toString() ?? '0') ?? + 0; + double lng = double.tryParse( + (data['longitude'] ?? data['lng'])?.toString() ?? '0') ?? + 0; + double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0; + + if (lat == 0 || lng == 0) return; + + LatLng newPos = LatLng(lat, lng); + + final nearbyDrivers = Get.find(); + if (nearbyDrivers.driverCarsLocationToPassengerAfterApplied.isEmpty) { + nearbyDrivers.driverCarsLocationToPassengerAfterApplied.add(newPos); + } else { + nearbyDrivers.driverCarsLocationToPassengerAfterApplied[0] = newPos; + } + + double speed = double.tryParse(data['speed']?.toString() ?? '0') ?? 0; + rideLifecycle.checkAndRecalculateIfDeviated( + newPos, + heading: heading, + speed: speed, + ); + + final mapEngine = Get.find(); + if (mapEngine.mapController != null) { + double zoom = 16.5; + if (speed > 0) { + zoom = 17.0 - ((speed - 10) / 70) * 2.5; + zoom = zoom.clamp(14.5, 17.0); + } + mapEngine.mapController! + .animateCamera(CameraUpdate.newLatLngZoom(newPos, zoom)); + } + + final dynamic distanceValue = + data['distance_m'] ?? data['distance_meters'] ?? data['distance']; + final double? distanceMeters = + double.tryParse(distanceValue?.toString() ?? ''); + final int? etaSeconds = data['eta_seconds'] == null + ? null + : int.tryParse(data['eta_seconds'].toString()); + final bool hasServerMetrics = (etaSeconds != null && etaSeconds > 0) || + (distanceMeters != null && distanceMeters > 0); + if (hasServerMetrics) { + rideLifecycle.updateDriverRouteMetrics( + etaSeconds: etaSeconds != null && etaSeconds > 0 ? etaSeconds : null, + distanceMeters: distanceMeters, + ); + } + + rideLifecycle.updateDriverMarker(newPos, heading); + rideLifecycle.updateRemainingRoute(newPos, updateEta: !hasServerMetrics); + rideLifecycle.update(); + } catch (e) { + Log.print('Error in handleDriverLocationUpdate: $e'); + } + } + + void disposeRideSocket() { + _heartbeatTimer?.cancel(); + _watchdogTimer?.cancel(); + if (_isSocketInitialized) { + socket.disconnect(); + socket.dispose(); + isSocketConnected = false; + _isSocketInitialized = false; + Log.print("🔌 Socket Disposed"); + } + } + + @override + void onClose() { + disposeRideSocket(); + super.onClose(); + } +} diff --git a/lib/controller/home/map/nearby_drivers_controller.dart b/lib/controller/home/map/nearby_drivers_controller.dart new file mode 100644 index 0000000..f989a30 --- /dev/null +++ b/lib/controller/home/map/nearby_drivers_controller.dart @@ -0,0 +1,475 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math' show Random, atan2, cos, pi, pow, sin, sqrt; +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; + +import '../../../constant/links.dart'; +import '../../../constant/api_key.dart'; +import '../../../print.dart'; +import '../../functions/crud.dart'; +import 'map_engine_controller.dart'; +import 'location_search_controller.dart'; +import 'ride_lifecycle_controller.dart'; +import '../../../models/model/locations.dart'; +import 'car_location.dart'; +import 'package:device_info_plus/device_info_plus.dart'; + +class NearbyDriversController extends GetxController { + List carsLocationByPassenger = []; + List driverCarsLocationToPassengerAfterApplied = []; + List carLocationsModels = []; + String? currentDriverMarkerId; + bool lowPerf = false; + + dynamic dataCarsLocationByPassenger; + bool noCarString = false; + final double minMovementThreshold = 2.0; + final Map _animationTimers = {}; + + final List> fakeCarData = []; + + Future getCarsLocationByPassengerAndReloadMarker() async { + carsLocationByPassenger = []; + final locSearch = Get.find(); + + if (locSearch.passengerLocation.latitude == 0 && locSearch.passengerLocation.longitude == 0) { + return false; + } + + var res = await CRUD().get( + link: AppLink.getCarsLocationByPassenger, + payload: { + 'lat': locSearch.passengerLocation.latitude.toString(), + 'lng': locSearch.passengerLocation.longitude.toString(), + 'radius': '5', + 'limit': '50', + }, + ); + + if (res == 'failure') { + noCarString = true; + dataCarsLocationByPassenger = 'failure'; + update(); + return false; + } + + noCarString = false; + var responseData = jsonDecode(res); + dataCarsLocationByPassenger = responseData; + + List driversList = []; + if (responseData['status'] == true && responseData['data'] != null) { + driversList = responseData['data']; + } else if (responseData['message'] != null) { + driversList = responseData['message']; + } + + final mapEngine = Get.find(); + + if (driversList.isEmpty) { + carsLocationByPassenger.clear(); + mapEngine.update(); + return false; + } + + carsLocationByPassenger.clear(); + + for (var i = 0; i < driversList.length; i++) { + var carData = driversList[i]; + + double lat = double.tryParse(carData['latitude'].toString()) ?? 0.0; + double lng = double.tryParse(carData['longitude'].toString()) ?? 0.0; + double heading = double.tryParse(carData['heading'].toString()) ?? 0.0; + + if (lat == 0.0 || lng == 0.0) continue; + + String driverId = (carData['driver_id'] ?? carData['id'] ?? '').toString(); + if (driverId.isEmpty || driverId == 'null') continue; + + _updateOrCreateMarker( + driverId, + LatLng(lat, lng), + heading, + _getIconForCar(carData), + ); + } + + mapEngine.update(); + return true; + } + + void _addFakeCarMarkers(LatLng center, int count) { + if (fakeCarData.isEmpty) { + Random random = Random(); + double radiusInKm = 2.5; + + for (int i = 0; i < count; i++) { + double angle = random.nextDouble() * 2 * pi; + double distance = sqrt(random.nextDouble()) * radiusInKm; + + double latOffset = (distance / 111.32); + double lonOffset = + (distance / (111.32 * cos(center.latitude * pi / 180.0))); + + double lat = center.latitude + (latOffset * cos(angle)); + double lon = center.longitude + (lonOffset * sin(angle)); + + double heading = random.nextDouble() * 360; + + fakeCarData.add({ + 'id': 'fake_$i', + 'latitude': lat, + 'longitude': lon, + 'heading': heading, + 'gender': 'Male', + }); + } + } + + for (var carData in fakeCarData) { + _updateOrCreateMarker( + carData['id'].toString(), + LatLng(carData['latitude'], carData['longitude']), + carData['heading'], + _getIconForCar(carData), + ); + } + } + + void addFakeCarMarkers(LatLng center, int count) { + _addFakeCarMarkers(center, count); + } + + Future getNearestDriverByPassengerLocation() async { + final rideLife = Get.find(); + final locSearch = Get.find(); + + if (!rideLife.rideConfirm) { + if (dataCarsLocationByPassenger != 'failure' && + dataCarsLocationByPassenger != null && + dataCarsLocationByPassenger.containsKey('message') && + dataCarsLocationByPassenger['message'] != null && + dataCarsLocationByPassenger['message'].length > 0) { + double nearestDistance = double.infinity; + CarLocation? nearestCar; + + for (var i = 0; + i < dataCarsLocationByPassenger['message'].length; + i++) { + var carLocation = dataCarsLocationByPassenger['message'][i]; + + try { + final distance = Geolocator.distanceBetween( + locSearch.passengerLocation.latitude, + locSearch.passengerLocation.longitude, + double.parse(carLocation['latitude']), + double.parse(carLocation['longitude']), + ); + + int durationToPassenger = (distance / 1000 / 25 * 3600).round(); + update(); + + if (distance < nearestDistance) { + nearestDistance = distance; + + nearestCar = CarLocation( + distance: distance, + duration: durationToPassenger.toDouble(), + id: carLocation['driver_id'], + latitude: double.parse(carLocation['latitude']), + longitude: double.parse(carLocation['longitude']), + ); + update(); + } + } catch (e) { + Log.print('Error calculating distance/duration: $e'); + } + } + return nearestCar; + } + } + return null; + } + + Future getNearestDriverByPassengerLocationAPIGOOGLE() async { + final rideLife = Get.find(); + final mapEngine = Get.find(); + final locSearch = Get.find(); + + if (mapEngine.polyLines.isEmpty || rideLife.totalCostPassenger == 0) { + return null; + } + if (!rideLife.rideConfirm) { + double nearestDistance = double.infinity; + if (dataCarsLocationByPassenger != 'failure' && + dataCarsLocationByPassenger != null && + dataCarsLocationByPassenger.containsKey('message') && + dataCarsLocationByPassenger['message'] != null) { + if (dataCarsLocationByPassenger['message'].length > 0) { + CarLocation? nearestCar; + for (var i = 0; + i < dataCarsLocationByPassenger['message'].length; + i++) { + var carLocation = dataCarsLocationByPassenger['message'][i]; + + update(); + String apiUrl = + '${AppLink.googleMapsLink}distancematrix/json?destinations=${carLocation['latitude']},${carLocation['longitude']}&origins=${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}&units=metric&key=${AK.mapAPIKEY}'; + var response = await CRUD().getGoogleApi(link: apiUrl, payload: {}); + if (response != null && response['status'] == "OK") { + var data = response; + int distance1 = + data['rows'][0]['elements'][0]['distance']['value']; + rideLife.distanceByPassenger = + data['rows'][0]['elements'][0]['distance']['text']; + rideLife.durationToPassenger = + data['rows'][0]['elements'][0]['duration']['value']; + + Duration durationFromDriverToPassenger = + Duration(seconds: rideLife.durationToPassenger.toInt()); + rideLife.stringRemainingTimeToPassenger = + data['rows'][0]['elements'][0]['duration']['text']; + update(); + if (distance1 < nearestDistance) { + nearestDistance = distance1.toDouble(); + + nearestCar = CarLocation( + distance: distance1.toDouble(), + duration: rideLife.durationToPassenger.toDouble(), + id: carLocation['driver_id'], + latitude: double.parse(carLocation['latitude']), + longitude: double.parse(carLocation['longitude']), + ); + update(); + } + } else { + Log.print('${response?['status']}: error Google distance matrix'); + } + } + return nearestCar; + } + } + } + return null; + } + + Future getCarForFirstConfirm(String carType) async { + bool foundCars = false; + int attempt = 0; + + Timer.periodic(const Duration(seconds: 4), (Timer t) async { + foundCars = await getCarsLocationByPassengerAndReloadMarker(); + Log.print('foundCars: $foundCars'); + + if (foundCars) { + t.cancel(); + } else if (attempt >= 4) { + t.cancel(); + if (!foundCars) { + noCarString = true; + dataCarsLocationByPassenger = 'failure'; + } + update(); + } + attempt++; + }); + } + + void startCarLocationSearch(String carType) { + int searchInterval = 5; + Log.print('searchInterval: $searchInterval'); + int boundIncreaseStep = 2500; + Log.print('boundIncreaseStep: $boundIncreaseStep'); + int maxAttempts = 3; + int maxBoundIncreaseStep = 6000; + int attempt = 0; + Log.print('initial attempt: $attempt'); + + Timer.periodic(Duration(seconds: searchInterval), (Timer timer) async { + Log.print('Current attempt: $attempt'); + bool foundCars = false; + final mapEngine = Get.find(); + if (attempt >= maxAttempts) { + timer.cancel(); + if (foundCars == false) { + noCarString = true; + update(); + } + } else if (mapEngine.reloadStartApp == true) { + Log.print('reloadStartApp: ${mapEngine.reloadStartApp}'); + foundCars = await getCarsLocationByPassengerAndReloadMarker(); + Log.print('foundCars: $foundCars'); + + if (foundCars) { + timer.cancel(); + } else { + attempt++; + Log.print('Incrementing attempt to: $attempt'); + + if (boundIncreaseStep < maxBoundIncreaseStep) { + boundIncreaseStep += 1500; + if (boundIncreaseStep > maxBoundIncreaseStep) { + boundIncreaseStep = maxBoundIncreaseStep; + } + Log.print('New boundIncreaseStep: $boundIncreaseStep'); + } + } + } + }); + } + + String _getIconForCar(Map carData) { + final mapEngine = Get.find(); + if (carData['model'].toString().contains('دراجة')) { + return mapEngine.motoIcon; + } else if (carData['gender'] == 'Female') { + return mapEngine.ladyIcon; + } else { + return mapEngine.carIcon; + } + } + + void _updateOrCreateMarker( + String markerId, LatLng newPosition, double newHeading, String icon) { + final mapEngine = Get.find(); + if (!mapEngine.isIconsLoaded) { + Log.print("⚠️ Skipping drawing marker $markerId because map icons are not fully loaded yet."); + return; + } + final mId = MarkerId(markerId); + final existingMarker = mapEngine.markers.cast().firstWhere( + (m) => m?.markerId == mId, + orElse: () => null, + ); + + if (existingMarker == null) { + mapEngine.markers = { + ...mapEngine.markers, + Marker( + markerId: mId, + position: newPosition, + rotation: newHeading, + icon: InlqBitmap.fromStyleImage(icon), + anchor: const Offset(0.5, 0.5), + ), + }; + mapEngine.update(); + } else { + double distance = Geolocator.distanceBetween( + existingMarker.position.latitude, + existingMarker.position.longitude, + newPosition.latitude, + newPosition.longitude); + if (distance >= minMovementThreshold) { + _smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon); + } + } + } + + void _smoothlyUpdateMarker( + Marker oldMarker, LatLng newPosition, double newHeading, String icon) { + final mapEngine = Get.find(); + final MarkerId markerIdKey = oldMarker.markerId; + + _animationTimers[markerIdKey.value]?.cancel(); + + int ticks = 0; + const int totalSteps = 20; + const int stepDuration = 50; + + double latStep = + (newPosition.latitude - oldMarker.position.latitude) / totalSteps; + double lngStep = + (newPosition.longitude - oldMarker.position.longitude) / totalSteps; + double headingStep = (newHeading - oldMarker.rotation) / totalSteps; + + LatLng currentPos = oldMarker.position; + double currentHeading = oldMarker.rotation; + + _animationTimers[markerIdKey.value] = + Timer.periodic(const Duration(milliseconds: stepDuration), (timer) { + ticks++; + + currentPos = + LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep); + currentHeading += headingStep; + + final updatedMarker = oldMarker.copyWith( + position: currentPos, + rotation: currentHeading, + icon: InlqBitmap.fromStyleImage(icon), + ); + + mapEngine.markers = { + ...mapEngine.markers.where((m) => m.markerId != markerIdKey), + updatedMarker, + }; + + if (mapEngine.mapController != null) { + mapEngine.mapController!.animateCamera(CameraUpdate.newLatLng(currentPos)); + } + + mapEngine.update(); + + if (ticks >= totalSteps) { + timer.cancel(); + _animationTimers.remove(markerIdKey.value); + } + }); + } + + double calculateBearing(double lat1, double lon1, double lat2, double lon2) { + double deltaLon = lon2 - lon1; + double y = sin(deltaLon) * cos(lat2); + double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(deltaLon); + double bearing = atan2(y, x); + return (bearing * 180 / pi + 360) % 360; + } + + void analyzeBehavior(Position currentPosition, List routePoints) { + double actualBearing = currentPosition.heading; + double expectedBearing = calculateBearing( + routePoints[0].latitude, + routePoints[0].longitude, + routePoints[1].latitude, + routePoints[1].longitude, + ); + + double bearingDifference = (expectedBearing - actualBearing).abs(); + if (bearingDifference > 30) { + Log.print("⚠️ السائق انحرف عن المسار!"); + } + } + + void detectStops(Position currentPosition) { + if (currentPosition.speed < 0.5) { + Log.print("🚦 السائق توقف في موقع غير متوقع!"); + } + } + + Future detectPerfMode() async { + try { + if (GetPlatform.isAndroid) { + final info = await DeviceInfoPlugin().androidInfo; + final sdk = info.version.sdkInt; + final ram = info.availableRamSize; + lowPerf = (sdk < 28) || (ram > 0 && ram < 3 * 1024 * 1024 * 1024); + } else { + lowPerf = false; + } + } catch (_) { + lowPerf = false; + } + update(); + } + + @override + void onClose() { + _animationTimers.forEach((key, timer) => timer.cancel()); + _animationTimers.clear(); + super.onClose(); + } +} diff --git a/lib/controller/home/map/ride_lifecycle_controller.dart b/lib/controller/home/map/ride_lifecycle_controller.dart new file mode 100644 index 0000000..c229ad2 --- /dev/null +++ b/lib/controller/home/map/ride_lifecycle_controller.dart @@ -0,0 +1,4558 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ui'; +import 'dart:math' show cos, max, min, pi, pow, sin, atan2; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; +import '../../../constant/api_key.dart'; +import '../../../services/offline_map_service.dart'; +import '../../../models/model/painter_copoun.dart'; +import '../../../views/widgets/mycircular.dart'; +import '../deep_link_controller.dart'; + +import '../../../constant/box_name.dart'; +import '../../../constant/links.dart'; +import '../../../constant/colors.dart'; +import '../../../constant/style.dart'; +import '../../../constant/country_polygons.dart'; +import '../../../env/env.dart'; +import '../../../main.dart'; // contains global 'box', 'sql' +import '../../../print.dart'; +import '../../../services/pip_service.dart'; +import '../../../services/ride_live_notification.dart'; +import '../../../views/home/map_page_passenger.dart'; +import '../../../views/Rate/rate_captain.dart'; +import '../../../views/Rate/rating_driver_bottom.dart'; +import '../../../views/widgets/mydialoug.dart'; +import '../../../views/widgets/elevated_btn.dart'; +import '../../../views/home/map_widget.dart/car_details_widget_to_go.dart'; +import '../../../views/home/map_widget.dart/select_driver_mishwari.dart'; +import '../../functions/crud.dart'; +import '../../functions/launch.dart'; +import '../../payment/payment_controller.dart'; +import '../points_for_rider_controller.dart'; +import 'map_engine_controller.dart'; +import 'location_search_controller.dart'; +import 'nearby_drivers_controller.dart'; +import 'ui_interactions_controller.dart'; +import 'map_socket_controller.dart'; +import '../decode_polyline_isolate.dart'; +import '../ios_live_activity_service.dart'; +import '../../firebase/local_notification.dart'; +import '../../firebase/notification_service.dart'; +import '../../functions/audio_record1.dart'; +import '../../functions/package_info.dart'; +import '../../functions/secure_storage.dart'; +import '../vip_waitting_page.dart'; +import '../device_performance.dart'; +import 'ride_state.dart'; +import '../../../views/widgets/error_snakbar.dart'; +import 'package:flutter_confetti/flutter_confetti.dart' hide Circle; +import 'package:crypto/crypto.dart'; + +class RideLifecycleController extends GetxController { + // --- Missing variables from monolithic controller --- + String currentRideId = ''; + bool isDrawingRoute = false; + bool isAnotherOreder = false; + bool isWhatsAppOrder = false; + LatLng startLocation = const LatLng(32, 35); + LatLng endLocation = const LatLng(32, 35); + String dynamicApiUrl = 'https://routec.intaleq.xyz/route'; + String? cardNumber; + bool isBeginRideFromDriverRunning = false; + bool isDriversTokensSend = false; + Map rideData = {}; + Map dInfo = {}; + List datadriverCarsLocationToPassengerAfterApplied = []; + double distanceOfTrip = 0.0; + double apiDistanceMeters = 0.0; + double tax = 0.0; + int selectedPassengerCount = 1; + final GlobalKey increaseFeeFormKey = GlobalKey(); + final GlobalKey messagesFormKey = GlobalKey(); + final GlobalKey promoFormKey = GlobalKey(); + String walletStr = '0'; + double walletVal = 0.0; + bool rideConfirm = false; + LatLng driverLocationToPassenger = const LatLng(32, 35); + final TextEditingController messageToDriver = TextEditingController(); + int carsOrder = 0; + + Rx currentRideState = RideState.noRide.obs; + String statusRide = 'wait'; + String statusRideVip = 'wait'; + bool statusRideFromStart = false; + + double distance = 0; + double duration = 0; + int durationToRide = 0; + int remainingTime = 25; + int remainingTimeToPassengerFromDriverAfterApplied = 60; + int remainingTimeDriverWaitPassenger5Minute = 60; + int timeToPassengerFromDriverAfterApplied = 0; + Timer? timerToPassengerFromDriverAfterApplied; + DateTime? _driverEtaUpdatedAt; + int _driverEtaSecondsAtUpdate = 0; + int _driverEtaCountdownTicks = 0; + + bool rideTimerBegin = false; + double progressTimerRideBegin = 0; + int remainingTimeTimerRideBegin = 60; + String stringRemainingTimeRideBegin = ''; + + late String rideId = 'yet'; + late String driverId = ''; + late String make = ''; + late String model = ''; + late String carColor = ''; + late String licensePlate = ''; + late String driverName = ''; + late String passengerName = ''; + late String driverPhone = ''; + late String colorHex = ''; + late String carYear = ''; + late String driverRate = '5.0'; + late String driverToken = ''; + + double kazan = 8; + double totalPassenger = 0; + double totalDriver = 0; + double costDistance = 0; + double costDuration = 0; + double averageDuration = 0; + double totalCostPassenger = 0; + + double totalPassengerSpeed = 0; + double totalPassengerBalash = 0; + double totalPassengerComfort = 0; + double totalPassengerElectric = 0; + double totalPassengerLady = 0; + double totalPassengerScooter = 0; + double totalPassengerVan = 0; + double totalPassengerRayehGai = 0; + double totalPassengerRayehGaiComfort = 0; + double totalPassengerRayehGaiBalash = 0; + + double latePrice = 0; + double fuelPrice = 0; + double heavyPrice = 0; + double naturePrice = 0; + + bool isRideFinished = false; + String stringRemainingTimeToPassenger = ''; + String stringRemainingTimeDriverWaitPassenger5Minute = ''; + + bool isDriverInPassengerWay = false; + bool isDriverArrivePassenger = false; + bool isSearchingWindow = false; + bool shouldFetch = true; + + double progressTimerToPassengerFromDriverAfterApplied = 0; + double progressTimerDriverWaitPassenger5Minute = 0; + bool isCashSelectedBeforeConfirmRide = false; + bool isPassengerChosen = false; + + Timer? _masterTimer; + Timer? _searchTimer; + Timer? _timer; + Timer? _uiCountdownTimer; + + bool _isArrivalProcessed = false; + bool _isFinishProcessed = false; + bool _isCancelProcessed = false; + bool _isAcceptanceProcessed = false; + bool _isRatingScreenOpen = false; + bool _isRecalculatingRoute = false; + + String _rideAcceptedViaSource = "Unknown"; + + final double kDurationScalar = 1.5348; + // مسافة الانحراف المسموح بها بالمتر قبل إعادة حساب المسار تلقائيًا للرحلة. + // إذا انحرف السائق عن المسار بأكثر من هذه المسافة، يُعاد حساب المسار. + final double _deviationThresholdMeters = 30.0; + int _routeHeadingMismatchCount = 0; + + final Map _pollingIntervals = { + RideState.noRide: 6, + RideState.searching: 8, + RideState.driverApplied: 10, + RideState.driverArrived: 15, + RideState.inProgress: 15, + RideState.cancelled: 3600, + RideState.finished: 3600, + RideState.preCheckReview: 3600, + }; + + Timer? _locationPollingTimer; + List _currentDriverRoutePoints = []; + double _currentDriverRouteDistanceMeters = 0.0; + int _currentDriverRouteDurationSeconds = 0; + + int _currentSearchPhase = 0; + bool _isFetchingDriverLocation = false; + Timer? _watchdogTimer; + + final List _searchRadii = [2400, 3000, 3100]; + final int _searchPhaseDurationSeconds = 30; + final int _totalSearchTimeoutSeconds = 90; + + int _noRideSearchCount = 0; + final int _noRideMaxTries = 3; + final int _noRideIntervalSec = 5; + DateTime? _noRideNextAllowed; + bool _noRideSearchCapped = false; + int _masterIntervalSeconds = -1; + + final StreamController _rideStatusStreamController = + StreamController.broadcast(); + Stream get rideStatusStream => _rideStatusStreamController.stream; + + final StreamController _beginRideStreamController = + StreamController.broadcast(); + Stream get beginRideStream => _beginRideStreamController.stream; + + final StreamController _timerStreamController = StreamController(); + Stream get timerStream => _timerStreamController.stream; + + bool isTimerFromDriverToPassengerAfterAppliedRunning = true; + bool isTimerRunning = false; + int beginRideInterval = 10; + + Timer? _rideProgressTimer; + bool _hasShownSpeedWarning = false; + bool rideInProgress = true; + double elapsedTimeInSeconds = 0; + String stringElapsedTimeRideBeginVip = '0:00'; + + Map rideStatusFromStartApp = {}; + bool isStartAppHasRide = false; + late Duration durationToAdd; + late DateTime newTime = DateTime.now(); + String durationByPassenger = ''; + int hours = 0; + int minutes = 0; + + int selectedReason = -1; + String? cancelNote; + double latitudeWhatsApp = 0; + double longitudeWhatsApp = 0; + + // Getters for linked controllers + LocationSearchController get locSearch => + Get.find(); + MapEngineController get mapEngine => Get.find(); + NearbyDriversController get nearbyDrivers => + Get.find(); + MapSocketController get mapSocket => Get.find(); + UiInteractionsController get uiInteractions => + Get.find(); + + // LocationSearchController pass-throughs + LatLng get passengerLocation => locSearch.passengerLocation; + set passengerLocation(LatLng val) => locSearch.passengerLocation = val; + + LatLng get newMyLocation => locSearch.newMyLocation; + set newMyLocation(LatLng val) => locSearch.newMyLocation = val; + + LatLng get newStartPointLocation => locSearch.newStartPointLocation; + set newStartPointLocation(LatLng val) => + locSearch.newStartPointLocation = val; + + LatLng get myDestination => locSearch.myDestination; + set myDestination(LatLng val) => locSearch.myDestination = val; + + String get startNameAddress => locSearch.startNameAddress; + set startNameAddress(String val) => locSearch.startNameAddress = val; + + String get endNameAddress => locSearch.endNameAddress; + set endNameAddress(String val) => locSearch.endNameAddress = val; + + List get placesCoordinate => locSearch.placesCoordinate; + set placesCoordinate(List val) => locSearch.placesCoordinate = val; + + int get activeMenuWaypointCount => locSearch.activeMenuWaypointCount; + set activeMenuWaypointCount(int val) => + locSearch.activeMenuWaypointCount = val; + + List get menuWaypoints => locSearch.menuWaypoints; + set menuWaypoints(List val) => locSearch.menuWaypoints = val; + + List get menuWaypointNames => locSearch.menuWaypointNames; + set menuWaypointNames(List val) => locSearch.menuWaypointNames = val; + + bool get passengerStartLocationFromMap => + locSearch.passengerStartLocationFromMap; + set passengerStartLocationFromMap(bool val) => + locSearch.passengerStartLocationFromMap = val; + + List get coordinatesWithoutEmpty => locSearch.coordinatesWithoutEmpty; + + // MapEngineController pass-throughs + Set get markers => mapEngine.markers; + set markers(Set val) { + mapEngine.markers = val; + mapEngine.update(); + } + + Set get polyLines => mapEngine.polyLines; + set polyLines(Set val) { + mapEngine.polyLines = val; + mapEngine.update(); + } + + IntaleqMapController? get mapController => mapEngine.mapController; + + bool get isStyleLoaded => mapEngine.isStyleLoaded; + set isStyleLoaded(bool val) => mapEngine.isStyleLoaded = val; + + bool get isBottomSheetShown => mapEngine.isBottomSheetShown; + set isBottomSheetShown(bool val) => mapEngine.isBottomSheetShown = val; + + double get heightBottomSheetShown => mapEngine.heightBottomSheetShown; + set heightBottomSheetShown(double val) => + mapEngine.heightBottomSheetShown = val; + + bool get isPickerShown => mapEngine.isPickerShown; + set isPickerShown(bool val) => mapEngine.isPickerShown = val; + + bool get isMarkersShown => mapEngine.isMarkersShown; + set isMarkersShown(bool val) => mapEngine.isMarkersShown = val; + + bool get isMainBottomMenuMap => mapEngine.isMainBottomMenuMap; + set isMainBottomMenuMap(bool val) => mapEngine.isMainBottomMenuMap = val; + + double get mainBottomMenuMapHeight => mapEngine.mainBottomMenuMapHeight; + set mainBottomMenuMapHeight(double val) => + mapEngine.mainBottomMenuMapHeight = val; + + bool get isWayPointSheet => mapEngine.isWayPointSheet; + set isWayPointSheet(bool val) => mapEngine.isWayPointSheet = val; + + bool get isWayPointStopsSheet => mapEngine.isWayPointStopsSheet; + set isWayPointStopsSheet(bool val) => mapEngine.isWayPointStopsSheet = val; + + bool get isWayPointStopsSheetUtilGetMap => + mapEngine.isWayPointStopsSheetUtilGetMap; + set isWayPointStopsSheetUtilGetMap(bool val) => + mapEngine.isWayPointStopsSheetUtilGetMap = val; + + double get wayPointSheetHeight => mapEngine.wayPointSheetHeight; + set wayPointSheetHeight(double val) => mapEngine.wayPointSheetHeight = val; + + double get cashConfirmPageShown => mapEngine.cashConfirmPageShown; + set cashConfirmPageShown(double val) => mapEngine.cashConfirmPageShown = val; + + bool get isCashConfirmPageShown => mapEngine.isCashConfirmPageShown; + set isCashConfirmPageShown(bool val) => + mapEngine.isCashConfirmPageShown = val; + + bool get isCancelRidePageShown => mapEngine.isCancelRidePageShown; + set isCancelRidePageShown(bool val) => mapEngine.isCancelRidePageShown = val; + + void changeCashConfirmPageShown() => mapEngine.changeCashConfirmPageShown(); + + void resetNoRideSearch() { + _noRideSearchCount = 0; + _noRideSearchCapped = false; + _noRideNextAllowed = null; + } + + double get paymentPageShown => mapEngine.paymentPageShown; + set paymentPageShown(double val) => mapEngine.paymentPageShown = val; + + void changeCancelRidePageShow() => mapEngine.changeCancelRidePageShow(); + + // NearbyDriversController pass-throughs + List get carsLocationByPassenger => nearbyDrivers.carsLocationByPassenger; + set carsLocationByPassenger(List val) => + nearbyDrivers.carsLocationByPassenger = val; + + List get driverCarsLocationToPassengerAfterApplied => + nearbyDrivers.driverCarsLocationToPassengerAfterApplied; + set driverCarsLocationToPassengerAfterApplied(List val) => + nearbyDrivers.driverCarsLocationToPassengerAfterApplied = val; + + bool get noCarString => nearbyDrivers.noCarString; + set noCarString(bool val) => nearbyDrivers.noCarString = val; + + double get speed => locSearch.speed; + set speed(double val) => locSearch.speed = val; + + Timer? get locationPollingTimer => _locationPollingTimer; + + bool isActiveRideState() { + return currentRideState.value == RideState.searching || + currentRideState.value == RideState.driverApplied || + currentRideState.value == RideState.driverArrived || + currentRideState.value == RideState.inProgress; + } + + void startMasterTimer() { + _masterTimer?.cancel(); + _masterTimer = Timer.periodic(const Duration(seconds: 13), (_) { + _handleRideState(currentRideState.value); + }); + } + + void cancelMasterTimer() { + _masterTimer?.cancel(); + _masterTimer = null; + } + + void startMasterTimerWithInterval(int seconds) { + if (_masterTimer != null && _masterIntervalSeconds == seconds) return; + _masterIntervalSeconds = seconds; + _masterTimer?.cancel(); + _masterTimer = Timer.periodic(Duration(seconds: seconds), (_) { + _handleRideState(currentRideState.value); + }); + } + + void stopAllTimers() { + Log.print('🛑 FORCE STOP: Stopping ALL Timers and Streams 🛑'); + _masterTimer?.cancel(); + _masterTimer = null; + timerToPassengerFromDriverAfterApplied?.cancel(); + timerToPassengerFromDriverAfterApplied = null; + _timer?.cancel(); + _timer = null; + _uiCountdownTimer?.cancel(); + _uiCountdownTimer = null; + _locationPollingTimer?.cancel(); + _locationPollingTimer = null; + _watchdogTimer?.cancel(); + _watchdogTimer = null; + _searchTimer?.cancel(); + _searchTimer = null; + _rideProgressTimer?.cancel(); + _rideProgressTimer = null; + + isTimerRunning = false; + isBeginRideFromDriverRunning = false; + _isFetchingDriverLocation = false; + update(); + } + + Future _handleRideState(RideState state) async { + if (_isRatingScreenOpen) { + Log.print('⛔ Rating Screen is Open. Skipping Logic.'); + stopAllTimers(); + return; + } + Log.print('Handling state: $state'); + + int effectivePollingInterval = _pollingIntervals[state] ?? 13; + + switch (state) { + case RideState.noRide: + final now = DateTime.now(); + if (_noRideSearchCount >= _noRideMaxTries) { + if (!_noRideSearchCapped) { + _noRideSearchCapped = true; + Log.print('[noRide] search capped at $_noRideMaxTries attempts'); + } + break; + } + if (_noRideNextAllowed != null && now.isBefore(_noRideNextAllowed!)) { + break; + } + _noRideSearchCount++; + Log.print('_noRideSearchCount: $_noRideSearchCount'); + _noRideNextAllowed = now.add(Duration(seconds: _noRideIntervalSec)); + nearbyDrivers.getCarsLocationByPassengerAndReloadMarker(); + nearbyDrivers.getNearestDriverByPassengerLocation(); + break; + + case RideState.cancelled: + stopAllTimers(); + break; + + case RideState.preCheckReview: + stopAllTimers(); + _checkLastRideForReview(); + break; + + case RideState.searching: + if (rideId == 'yet' || rideId.isEmpty) break; + try { + String statusFromServer = await getRideStatus(rideId); + if (statusFromServer == 'Apply' || statusFromServer == 'Applied') { + await processRideAcceptance(source: "Polling"); + break; + } + } catch (e) { + Log.print('Error polling getRideStatus: $e'); + } + + final now = DateTime.now(); + final int elapsedSeconds = now.difference(_searchStartTime!).inSeconds; + + if (elapsedSeconds > _totalSearchTimeoutSeconds) { + stopAllTimers(); + currentRideState.value = RideState.noRide; + isSearchingWindow = false; + update(); + _showIncreaseFeeDialog(); + break; + } + + int targetPhase = + (elapsedSeconds / _searchPhaseDurationSeconds).floor(); + if (targetPhase >= _searchRadii.length) { + targetPhase = _searchRadii.length - 1; + } + + bool isNewPhase = targetPhase > _currentSearchPhase; + bool timeToScanForNewDrivers = (elapsedSeconds % 15 == 0); + + if (isNewPhase || timeToScanForNewDrivers || elapsedSeconds < 5) { + _currentSearchPhase = targetPhase; + int currentRadius = _searchRadii[_currentSearchPhase]; + Log.print( + '[Search Logic] Scanning for drivers. Phase: $_currentSearchPhase, Radius: $currentRadius'); + } + + if (elapsedSeconds < 5) { + driversStatusForSearchWindow = 'Your order is being prepared'.tr; + } else if (elapsedSeconds < 15) { + driversStatusForSearchWindow = 'Your order sent to drivers'.tr; + } else { + driversStatusForSearchWindow = + 'The drivers are reviewing your request'.tr; + } + update(); + break; + + case RideState.driverApplied: + if (!_isDriverAppliedLogicExecuted && !_isAcceptanceProcessed) { + Log.print('[handleRideState] Execution driverApplied logic.'); + rideAppliedFromDriver(true); + _isDriverAppliedLogicExecuted = true; + } + + if (!mapSocket.isSocketConnected) { + try { + String statusFromServer = await getRideStatus(rideId); + if (statusFromServer == 'Arrived') { + currentRideState.value = RideState.driverArrived; + break; + } else if (statusFromServer == 'Begin' || + statusFromServer == 'inProgress') { + processRideBegin(); + break; + } + } catch (e) { + Log.print('Error polling for Arrived/Begin status: $e'); + } + } + if (!_isSocketHealthy()) { + getDriverCarsLocationToPassengerAfterApplied(); + } + break; + + case RideState.driverArrived: + if (!_isDriverArrivedLogicExecuted) { + _isDriverArrivedLogicExecuted = true; + startTimerDriverWaitPassenger5Minute(); + uiInteractions.driverArrivePassengerDialoge(); + } + break; + + case RideState.inProgress: + if (!mapSocket.isSocketConnected) { + try { + String statusFromServer = await getRideStatus(rideId); + if (statusFromServer == 'Finished' || + statusFromServer == 'finished') { + Log.print( + '🏁 DETECTED FINISHED: Killing processes and forcing Review.'); + stopAllTimers(); + currentRideState.value = RideState.preCheckReview; + tripFinishedFromDriver(); + _checkLastRideForReview(); + return; + } + } catch (e) { + Log.print('Error polling status: $e'); + } + } + + if (!_isRideBeginLogicExecuted) { + _isRideBeginLogicExecuted = true; + _executeBeginRideLogic(); + } + if (!_isSocketHealthy()) { + getDriverCarsLocationToPassengerAfterApplied(); + } + break; + + case RideState.finished: + tripFinishedFromDriver(); + stopAllTimers(); + effectivePollingInterval = 3600; + break; + } + startMasterTimerWithInterval(effectivePollingInterval); + } + + bool _isSocketHealthy() { + return mapSocket.isSocketHealthy(); + } + + Future _checkInitialRideStatus() async { + await getRideStatusFromStartApp(); + if (rideStatusFromStartApp['data'] == null) { + currentRideState.value = RideState.noRide; + _handleRideState(currentRideState.value); + return; + } + String _status = rideStatusFromStartApp['data']['status'] ?? ''; + String _lowerStatus = _status.toLowerCase(); + + if (_lowerStatus == 'waiting' || + _lowerStatus == 'apply' || + _lowerStatus == 'applied' || + _lowerStatus == 'accepted' || + _lowerStatus == 'arrived' || + _lowerStatus == 'begin') { + rideId = rideStatusFromStartApp['data']['rideId'].toString(); + currentRideState.value = _lowerStatus == 'waiting' + ? RideState.searching + : (_lowerStatus == 'apply' || + _lowerStatus == 'applied' || + _lowerStatus == 'accepted') + ? RideState.driverApplied + : _lowerStatus == 'arrived' + ? RideState.driverArrived + : _lowerStatus == 'begin' + ? RideState.inProgress + : _lowerStatus == 'cancel' + ? RideState.cancelled + : RideState.noRide; + } else if (_lowerStatus == 'finished') { + if (rideStatusFromStartApp['data']['needsReview'] == 1) { + currentRideState.value = RideState.preCheckReview; + } else { + currentRideState.value = RideState.noRide; + } + } else { + currentRideState.value = RideState.noRide; + } + _handleRideState(currentRideState.value); + } + + Future _checkLastRideForReview() async { + Log.print('⭐ FORCE OPEN RATING PAGE (Get.to mode)'); + await getRideStatusFromStartApp(); + + if (rideStatusFromStartApp['data'] == null) { + currentRideState.value = RideState.noRide; + startMasterTimer(); + return; + } + + String needsReview = + rideStatusFromStartApp['data']['needsReview'].toString(); + + if (needsReview == '1') { + _isRatingScreenOpen = true; + var args = { + 'driverId': rideStatusFromStartApp['data']['driver_id'].toString(), + 'rideId': rideStatusFromStartApp['data']['rideId'].toString(), + 'driverName': rideStatusFromStartApp['data']['driverName'], + 'price': rideStatusFromStartApp['data']['price'], + }; + + await Get.to( + () => RatingDriverBottomSheet(), + arguments: args, + preventDuplicates: true, + popGesture: false, + ); + + Log.print('✅ Rating Page Closed. Resetting App.'); + _isRatingScreenOpen = false; + restCounter(); + currentRideState.value = RideState.noRide; + startMasterTimer(); + } else { + currentRideState.value = RideState.noRide; + startMasterTimer(); + } + } + + DateTime? _searchStartTime; + bool _isDriverAppliedLogicExecuted = false; + bool _isDriverArrivedLogicExecuted = false; + bool _isRideBeginLogicExecuted = false; + String driversStatusForSearchWindow = ''; + + void startSearchingForDriver() async { + if (currentRideState.value == RideState.searching) return; + + isSearchingWindow = true; + currentRideState.value = RideState.searching; + driversStatusForSearchWindow = 'Searching for nearby drivers...'.tr; + _searchStartTime = DateTime.now(); + _currentSearchPhase = 0; + update(); + + bool rideCreated = await postRideDetailsToServer(); + + if (!rideCreated) { + isSearchingWindow = false; + currentRideState.value = RideState.noRide; + mySnackbarWarning("Could not create ride. Please try again.".tr); + update(); + return; + } + + _addRideToWaitingTable(); + mapSocket.initConnectionWithSocket(); + } + + void _showIncreaseFeeDialog() { + Get.dialog( + CupertinoAlertDialog( + title: Text("No drivers accepted your request yet".tr), + content: Text( + "Increasing the fare might attract more drivers. Would you like to increase the price?" + .tr), + actions: [ + CupertinoDialogAction( + child: Text("Cancel Ride".tr, + style: TextStyle(color: AppColor.redColor)), + onPressed: () { + Get.back(); + mapEngine.changeCancelRidePageShow(); + }, + ), + CupertinoDialogAction( + child: Text("Increase Fare".tr, + style: TextStyle(color: AppColor.greenColor)), + onPressed: () { + Get.back(); + double newPrice = totalPassenger * 1.10; + increasePriceAndRestartSearch(newPrice); + }, + ), + ], + ), + barrierDismissible: false, + ); + } + + Future increasePriceAndRestartSearch(double newPrice) async { + totalPassenger = newPrice; + update(); + + await CRUD() + .post(link: "${AppLink.server}/ride/rides/update.php", payload: { + "id": rideId, + "price": newPrice.toStringAsFixed(2), + }); + + Log.print( + '[increasePrice] Price changed. Clearing notified list to resend.'); + notifiedDrivers.clear(); + + _searchStartTime = DateTime.now(); + _currentSearchPhase = 0; + isSearchingWindow = true; + update(); + startMasterTimer(); + } + + void _stopWaitPassengerTimer({bool resetUI = false}) { + _waitPassengerTimer?.cancel(); + _waitPassengerTimer = null; + + if (resetUI) { + progressTimerDriverWaitPassenger5Minute = 0.0; + remainingTimeDriverWaitPassenger5Minute = 0; + stringRemainingTimeDriverWaitPassenger5Minute = '00:00'; + update(); + } + } + + void _executeBeginRideLogic() { + Log.print('[executeBeginRideLogic] execution of ride start logic...'); + _stopWaitPassengerTimer(resetUI: true); + + timeToPassengerFromDriverAfterApplied = 0; + remainingTime = 0; + remainingTimeToPassengerFromDriverAfterApplied = 0; + remainingTimeDriverWaitPassenger5Minute = 0; + + rideTimerBegin = true; + statusRide = 'Begin'; + isDriverInPassengerWay = false; + isDriverArrivePassenger = false; + + box.write(BoxName.passengerWalletTotal, '0'); + update(); + + rideIsBeginPassengerTimer(); + runWhenRideIsBegin(); + + NotificationController().showNotification( + 'Trip is Begin'.tr, + 'The trip has started! Feel free to contact emergency numbers, share your trip, or activate voice recording for the journey' + .tr, + 'start'); + } + + Future processRideBegin({String source = "Unknown"}) async { + if (currentRideState.value == RideState.inProgress || + _isRideStartedProcessed) { + return; + } + _isRideStartedProcessed = true; + currentRideState.value = RideState.inProgress; + statusRide = 'Begin'; + + remainingTimeDriverWaitPassenger5Minute = 0; + _stopWaitPassengerTimer(); + + // مسح الخطوط القديمة (pickup/direct) قبل رسم خط المرحلة الجديدة + polyLines = polyLines + .where((p) => + p.polylineId.value != 'main_route' && + p.polylineId.value != 'route_direct' && + !p.polylineId.value.startsWith('driver_route')) + .toSet(); + + // موقع السائق الحالي من آخر تحديث + LatLng driverPos = passengerLocation; + if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) { + driverPos = driverCarsLocationToPassengerAfterApplied.last; + } + + // رسم المسار من موقع السائق إلى الهدف بخط أزرق مستمر + await calculateDriverToPassengerRoute(driverPos, myDestination, + isBeginPhase: true); + + rideIsBeginPassengerTimer(); + runWhenRideIsBegin(); + update(); + } + + bool _isRideStartedProcessed = false; + + void updateDriverRouteMetrics({int? etaSeconds, double? distanceMeters}) { + if (distanceMeters != null && distanceMeters > 0) { + distanceByPassenger = distanceMeters.toStringAsFixed(0); + } + + if (etaSeconds == null) return; + + final int clampedEta = max(0, etaSeconds); + timeToPassengerFromDriverAfterApplied = clampedEta; + remainingTimeToPassengerFromDriverAfterApplied = clampedEta; + durationToPassenger = clampedEta; + _driverEtaSecondsAtUpdate = clampedEta; + _driverEtaUpdatedAt = DateTime.now(); + + final int minutes = (clampedEta / 60).floor(); + final int seconds = clampedEta % 60; + stringRemainingTimeToPassenger = + '$minutes:${seconds.toString().padLeft(2, '0')}'; + } + + void startTimerFromDriverToPassengerAfterApplied() { + stopTimerFromDriverToPassengerAfterApplied(); + if (isTimerRunning) return; + isTimerRunning = true; + isTimerFromDriverToPassengerAfterAppliedRunning = true; + _driverEtaUpdatedAt ??= DateTime.now(); + _driverEtaSecondsAtUpdate = timeToPassengerFromDriverAfterApplied; + _driverEtaCountdownTicks = 0; + + timerToPassengerFromDriverAfterApplied = + Timer.periodic(const Duration(seconds: 1), (timer) { + bool isRideActive = (statusRide == 'Apply' || + statusRide == 'Arrived' || + statusRide == 'Begin' || + currentRideState.value == RideState.driverApplied || + currentRideState.value == RideState.driverArrived || + currentRideState.value == RideState.inProgress); + + if (!isRideActive || !isTimerFromDriverToPassengerAfterAppliedRunning) { + timer.cancel(); + timerToPassengerFromDriverAfterApplied = null; + isTimerRunning = false; + return; + } + + _driverEtaCountdownTicks++; + if (!_timerStreamController.isClosed) { + _timerStreamController.add(_driverEtaCountdownTicks); + } + + final int secondsElapsedSinceEta = _driverEtaUpdatedAt == null + ? 0 + : DateTime.now().difference(_driverEtaUpdatedAt!).inSeconds; + remainingTimeToPassengerFromDriverAfterApplied = + _driverEtaSecondsAtUpdate - secondsElapsedSinceEta; + + if (remainingTimeToPassengerFromDriverAfterApplied < 0) { + remainingTimeToPassengerFromDriverAfterApplied = 0; + } + + int minutes = + (remainingTimeToPassengerFromDriverAfterApplied / 60).floor(); + int seconds = remainingTimeToPassengerFromDriverAfterApplied % 60; + stringRemainingTimeToPassenger = + '$minutes:${seconds.toString().padLeft(2, '0')}'; + + if (_driverEtaCountdownTicks % 5 == 0) { + double currentProgress = 1 - + (remainingTimeToPassengerFromDriverAfterApplied / + (_driverEtaSecondsAtUpdate == 0 + ? 1 + : _driverEtaSecondsAtUpdate)); + + IosLiveActivityService.updateRideActivity( + status: 'waiting', + driverName: driverName, + carDetails: '$make • $model • $carColor', + etaText: stringRemainingTimeToPassenger, + progress: currentProgress.clamp(0.0, 1.0), + ); + } + + if (_driverEtaCountdownTicks % beginRideInterval == 0) { + uploadPassengerLocation(); + } else { + update(); + } + }); + } + + void stopTimerFromDriverToPassengerAfterApplied() { + isTimerFromDriverToPassengerAfterAppliedRunning = false; + timerToPassengerFromDriverAfterApplied?.cancel(); + timerToPassengerFromDriverAfterApplied = null; + isTimerRunning = false; + update(); + } + + Timer? _waitPassengerTimer; + static const int _waitPassengerTotalSeconds = 300; + int _waitPassengerElapsedSeconds = 0; + + void startTimerDriverWaitPassenger5Minute() { + if (currentRideState.value != RideState.driverArrived) return; + + stopTimerFromDriverToPassengerAfterApplied(); + isTimerRunning = false; + _stopWaitPassengerTimer(); + + isDriverArrivePassenger = true; + isDriverInPassengerWay = false; + timeToPassengerFromDriverAfterApplied = 0; + + _waitPassengerElapsedSeconds = 0; + remainingTimeDriverWaitPassenger5Minute = _waitPassengerTotalSeconds; + progressTimerDriverWaitPassenger5Minute = 0; + + int m = (remainingTimeDriverWaitPassenger5Minute / 60).floor(); + int s = remainingTimeDriverWaitPassenger5Minute % 60; + stringRemainingTimeDriverWaitPassenger5Minute = + '$m:${s.toString().padLeft(2, '0')}'; + + update(); + + _waitPassengerTimer = Timer.periodic(const Duration(seconds: 1), (t) { + if (currentRideState.value != RideState.driverArrived) { + _stopWaitPassengerTimer(resetUI: true); + if (currentRideState.value == RideState.inProgress) { + isDriverArrivePassenger = false; + } + update(); + return; + } + + _waitPassengerElapsedSeconds++; + int remaining = _waitPassengerTotalSeconds - _waitPassengerElapsedSeconds; + if (remaining < 0) remaining = 0; + + remainingTimeDriverWaitPassenger5Minute = remaining; + progressTimerDriverWaitPassenger5Minute = + _waitPassengerElapsedSeconds / _waitPassengerTotalSeconds; + + int minutes = (remaining / 60).floor(); + int seconds = remaining % 60; + stringRemainingTimeDriverWaitPassenger5Minute = + '$minutes:${seconds.toString().padLeft(2, '0')}'; + update(); + + if (remaining == 0) { + _stopWaitPassengerTimer(); + } + }); + } + + void beginRideTimer() { + Timer.periodic(const Duration(seconds: 1), (timer) { + if (!timerController.isClosed) { + timerController.add(timer.tick); + } + update(); + }); + } + + final timerController = StreamController(); + void stopRideTimer() { + timerController.close(); + update(); + } + + void rideIsBeginPassengerTimer() { + _rideProgressTimer?.cancel(); + _hasShownSpeedWarning = false; + + DateTime now = DateTime.now(); + DateTime expectedArrivalTime = now.add(Duration(seconds: durationToRide)); + + var arrivalTime = DateFormat('hh:mm a').format(expectedArrivalTime); + box.write(BoxName.arrivalTime, arrivalTime); + + Log.print("⏳ Ride Timer Started. Duration: $durationToRide sec"); + + _rideProgressTimer = + Timer.periodic(const Duration(seconds: 1), (timer) async { + if (currentRideState.value != RideState.inProgress) { + timer.cancel(); + return; + } + + DateTime currentNow = DateTime.now(); + int remainingSeconds = + expectedArrivalTime.difference(currentNow).inSeconds; + + if (remainingSeconds < 0) remainingSeconds = 0; + + remainingTimeTimerRideBegin = remainingSeconds; + progressTimerRideBegin = + durationToRide > 0 ? 1 - (remainingSeconds / durationToRide) : 1.0; + + int minutes = (remainingSeconds / 60).floor(); + int seconds = remainingSeconds % 60; + stringRemainingTimeRideBegin = + '$minutes:${seconds.toString().padLeft(2, '0')}'; + + final percent = (progressTimerRideBegin * 100).clamp(0, 100).toInt(); + + if (remainingSeconds % 5 == 0 || remainingSeconds == 0) { + IosLiveActivityService.updateRideActivity( + status: 'ongoing', + driverName: driverName, + carDetails: '$make • $model • $carColor', + etaText: stringRemainingTimeRideBegin, + progress: progressTimerRideBegin.clamp(0.0, 1.0), + ); + } + + if (remainingSeconds % 60 == 0 || remainingSeconds == 0) { + await RideLiveNotification.showTripInProgress( + percentage: percent, + etaText: stringRemainingTimeRideBegin, + ); + } + + if (speed > 100 && !_hasShownSpeedWarning) { + _hasShownSpeedWarning = true; + _triggerSpeedWarning(); + } + + if (speed < 80 && _hasShownSpeedWarning) { + _hasShownSpeedWarning = false; + } + + if (remainingSeconds <= 0) { + timer.cancel(); + } + update(); + }); + } + + void _triggerSpeedWarning() { + NotificationController().showNotification("Warning: Speeding detected!".tr, + 'You can call or record audio of this trip'.tr, 'tone1'); + + Get.defaultDialog( + barrierDismissible: false, + title: "Warning: Speeding detected!".tr, + titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), + content: Column( + children: [ + Icon(Icons.speed, size: 50, color: AppColor.redColor), + const SizedBox(height: 10), + Text( + "We noticed the speed is exceeding 100 km/h. Please slow down for your safety..." + .tr, + textAlign: TextAlign.center, + style: AppStyle.title, + ), + ], + ), + confirm: MyElevatedButton( + title: "Share Trip Details".tr, + kolor: AppColor.redColor, + onPressed: () { + Get.back(); + uiInteractions.sosPassenger(); + }, + ), + cancel: MyElevatedButton( + title: "I'm Safe".tr, + kolor: AppColor.greenColor, + onPressed: () { + Get.back(); + }, + ), + ); + } + + void rideIsBeginPassengerTimerVIP() async { + rideInProgress = true; + bool sendSOS = false; + while (rideInProgress) { + await Future.delayed(const Duration(seconds: 1)); + elapsedTimeInSeconds++; + + int minutes = (elapsedTimeInSeconds / 60).floor(); + int seconds = (elapsedTimeInSeconds % 60).toInt(); + stringElapsedTimeRideBeginVip = + '$minutes:${seconds.toString().padLeft(2, '0')}'; + + if (speed > 100 && !sendSOS) { + Get.defaultDialog( + barrierDismissible: false, + title: "Warning: Speeding detected!".tr, + titleStyle: AppStyle.title, + content: Text( + "We noticed the speed is exceeding 100 km/h. Please slow down for your safety. If you feel unsafe, you can share your trip details with a contact or call the police using the red SOS button." + .tr, + style: AppStyle.title, + ), + confirm: MyElevatedButton( + title: "Share Trip Details".tr, + onPressed: () { + Get.back(); + String message = "**Emergency SOS from Passenger:**\n"; + message += "* ${'Origin'.tr}: $passengerLocation\n"; + message += "* ${'Destination'.tr}: $myDestination\n"; + message += "* ${'Driver Name'.tr}: $passengerName\n"; + message += "* ${'Driver Car Plate'.tr}: $licensePlate\n\n"; + message += "* ${'Driver Phone'.tr}: $driverPhone\n\n"; + message += + "${'Current Location'.tr}:https://www.google.com/maps/place/${passengerLocation.latitude},${passengerLocation.longitude} \n"; + message += "Please help! Contact me as soon as possible.".tr; + + launchCommunication( + 'whatsapp', box.read(BoxName.sosPhonePassenger), message); + sendSOS = true; + }, + kolor: AppColor.redColor, + ), + cancel: MyElevatedButton( + title: "Cancel".tr, + onPressed: () { + Get.back(); + }, + kolor: AppColor.greenColor, + ), + ); + } + update(); + } + } + + Future tripFinishedFromDriver() async { + Log.print('🧹 Cleaning UI for Finish'); + if (Get.isDialogOpen == true) Get.back(); + if (Get.isBottomSheetOpen == true) Get.back(); + + statusRide = 'Finished'; + currentRideState.value = RideState.finished; + + isSearchingWindow = false; + rideTimerBegin = false; + shouldFetch = false; + + stopAllTimers(); + resetAllMapStates(); + mapEngine.clearPolyline(); + markers = {}; + update(); + } + + void listenToBeginRideStream() { + beginRideStream.listen((status) { + Log.print("Ride status: $status"); + }, onError: (error) { + Log.print("Error in Begin Ride Stream: $error"); + }); + } + + Future begiVIPTripFromPassenger() async { + timeToPassengerFromDriverAfterApplied = 0; + remainingTime = 0; + isBottomSheetShown = false; + remainingTimeToPassengerFromDriverAfterApplied = 0; + remainingTimeDriverWaitPassenger5Minute = 0; + rideTimerBegin = true; + statusRideVip = 'Begin'; + isDriverInPassengerWay = false; + isDriverArrivePassenger = false; + update(); + rideIsBeginPassengerTimerVIP(); + runWhenRideIsBegin(); + } + + Future getRideStatusFromStartApp() async { + try { + var res = await CRUD().get( + link: AppLink.getRideStatusFromStartApp, + payload: {'passenger_id': box.read(BoxName.passengerID)}); + Log.print('rideStatusFromStartApp: $res'); + if (res == 'failure') { + rideStatusFromStartApp = { + 'data': {'status': 'NoRide', 'needsReview': false} + }; + isStartAppHasRide = false; + } else { + var decoded = jsonDecode(res); + if (decoded['status'] == 'failure') { + rideStatusFromStartApp = { + 'data': {'status': 'NoRide', 'needsReview': false} + }; + isStartAppHasRide = false; + } else { + rideStatusFromStartApp = decoded; + } + } + + String status = + (rideStatusFromStartApp['data']['status'] ?? '').toString(); + String lowerStatus = status.toLowerCase(); + + if (lowerStatus == 'begin' || + lowerStatus == 'apply' || + lowerStatus == 'applied' || + lowerStatus == 'accepted' || + lowerStatus == 'arrived') { + statusRide = status; + isStartAppHasRide = true; + final bool isBeginStatus = lowerStatus == 'begin'; + final bool isPickupStatus = lowerStatus == 'apply' || + lowerStatus == 'applied' || + lowerStatus == 'accepted' || + lowerStatus == 'arrived'; + currentRideState.value = lowerStatus == 'begin' + ? RideState.inProgress + : lowerStatus == 'arrived' + ? RideState.driverArrived + : RideState.driverApplied; + driverId = + rideStatusFromStartApp['data']['driver_id']?.toString() ?? ''; + driverName = + rideStatusFromStartApp['data']['driverName']?.toString() ?? ''; + driverRate = + rideStatusFromStartApp['data']['rateDriver']?.toString() ?? '5.0'; + final LatLng? pickupPoint = _parseLatLng( + rideStatusFromStartApp['data']['start_location']?.toString()); + final LatLng? destinationPoint = _parseLatLng( + rideStatusFromStartApp['data']['end_location']?.toString()); + if (pickupPoint != null) { + passengerLocation = pickupPoint; + } + if (destinationPoint != null) { + myDestination = destinationPoint; + } + statusRideFromStart = true; + update(); + + // Safe recovery of trip data + Map? tripData; + try { + var rawTrip = box.read(BoxName.tripData); + if (rawTrip is Map) { + tripData = Map.from(rawTrip); + } + } catch (e) { + Log.print("Error reading BoxName.tripData: $e"); + } + + String? pointsString = tripData?['polyline']; + + if (pointsString == null || pointsString.isEmpty) { + // No local polyline saved: Re-fetch the route from API + final String startLoc = + rideStatusFromStartApp['data']['start_location'] ?? ''; + final String endLoc = + rideStatusFromStartApp['data']['end_location'] ?? ''; + if (startLoc.isNotEmpty && endLoc.isNotEmpty) { + Log.print("🔄 Re-fetching route from API: $startLoc -> $endLoc"); + // Call getDirectionMap to fetch the route asynchronously + getDirectionMap(startLoc, endLoc, []); + } + } else { + List decodedPoints = + await compute(decodePolylineIsolate, pointsString); + + mapEngine.clearPolyline(); + mapEngine.polylineCoordinates.clear(); + mapEngine.polylineCoordinates.addAll(decodedPoints); + if (decodedPoints.isNotEmpty) { + passengerLocation = pickupPoint ?? decodedPoints.first; + myDestination = destinationPoint ?? decodedPoints.last; + } + var polyline = Polyline( + polylineId: const PolylineId('main_route'), + points: mapEngine.polylineCoordinates, + width: 6, + color: const Color(0xFF2196F3), + ); + + polyLines = {...polyLines, polyline}; + } + + timeToPassengerFromDriverAfterApplied = 0; + remainingTime = 0; + remainingTimeToPassengerFromDriverAfterApplied = 0; + remainingTimeDriverWaitPassenger5Minute = 0; + rideTimerBegin = isBeginStatus; + isDriverInPassengerWay = false; + isDriverArrivePassenger = false; + + // Safe durationToAdd parsing + if (tripData != null && tripData['distance_m'] != null) { + var distVal = tripData['distance_m']; + if (distVal is Duration) { + durationToAdd = distVal; + } else if (distVal is num) { + durationToAdd = Duration(seconds: distVal.toInt()); + } + } else { + durationToAdd = Duration.zero; + } + + mapSocket.initConnectionWithSocket(); + + if (isBeginStatus) { + _isRideBeginLogicExecuted = true; + _isRideStartedProcessed = true; + await getDriverCarsLocationToPassengerAfterApplied(); + if (driverCarsLocationToPassengerAfterApplied.isNotEmpty && + myDestination.latitude != 0 && + myDestination.longitude != 0) { + await calculateDriverToPassengerRoute( + driverCarsLocationToPassengerAfterApplied.last, + myDestination, + isBeginPhase: true, + ); + } + rideIsBeginPassengerTimer(); + runWhenRideIsBegin(); + } else if (isPickupStatus) { + _isAcceptanceProcessed = true; + _isDriverAppliedLogicExecuted = true; + await getDriverCarsLocationToPassengerAfterApplied(); + if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) { + await calculateDriverToPassengerRoute( + driverCarsLocationToPassengerAfterApplied.last, + passengerLocation, + ); + startTimerFromDriverToPassengerAfterApplied(); + } + _startSocketWatchdog(); + } + update(); + } + } catch (e) { + Log.print("Error getRideStatusFromStartApp: $e"); + } + } + + void driverArrivePassenger() { + timeToPassengerFromDriverAfterApplied = 0; + remainingTime = 0; + update(); + rideIsBeginPassengerTimer(); + } + + void cancelTimerToPassengerFromDriverAfterApplied() { + timerToPassengerFromDriverAfterApplied?.cancel(); + } + + Future postRideDetailsToServer() async { + if (mapEngine.polylineCoordinates.isEmpty) return false; + + LatLng startLoc = mapEngine.polylineCoordinates.first; + LatLng endLoc = mapEngine.polylineCoordinates.last; + + Map payload = { + "start_location": '${startLoc.latitude},${startLoc.longitude}', + "end_location": '${endLoc.latitude},${endLoc.longitude}', + "date": DateTime.now().toString(), + "time": DateTime.now().toString(), + "endtime": "00:00:00", + "price": totalPassenger.toStringAsFixed(2), + "passenger_id": box.read(BoxName.passengerID).toString(), + "driver_id": "0", + "status": "waiting", + "carType": box.read(BoxName.carType), + "price_for_driver": totalPassenger.toString(), + "price_for_passenger": totalME.toString(), + "distance": distance.toString(), + "passenger_name": box.read(BoxName.name).toString(), + "passenger_phone": box.read(BoxName.phone).toString(), + "passenger_token": box.read(BoxName.tokenFCM).toString(), + "passenger_email": box.read(BoxName.email).toString(), + "passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(), + "passenger_rating": (passengerRate ?? 5.0).toString(), + "start_name": startNameAddress, + "end_name": endNameAddress, + "duration_text": "${(durationToRide / 60).floor()}", + "distance_text": "$distance", + "is_wallet": Get.find().isWalletChecked.toString(), + "has_steps": Get.find().wayPoints.length > 1 + ? 'true' + : 'false', + "step0": placesCoordinate.length > 0 ? placesCoordinate[0] : "", + "step1": placesCoordinate.length > 1 ? placesCoordinate[1] : "", + "step2": placesCoordinate.length > 2 ? placesCoordinate[2] : "", + "step3": placesCoordinate.length > 3 ? placesCoordinate[3] : "", + "step4": placesCoordinate.length > 4 ? placesCoordinate[4] : "", + }; + + Log.print(' 📦 Payload: $payload'); + + try { + var response = await CRUD().post( + link: "${AppLink.server}/ride/rides/add_ride.php", payload: payload); + + var jsonResponse = (response is String) ? jsonDecode(response) : response; + + if (jsonResponse['status'] == 'success') { + rideId = jsonResponse['message'].toString(); + Log.print("✅ Ride Created ID: $rideId"); + return true; + } else { + Log.print("❌ Ride Creation Failed: $response"); + return false; + } + } catch (e) { + Log.print("❌ Exception in postRide: $e"); + return false; + } + } + + Future rideAppliedFromDriver(bool isApplied) async { + Log.print('[rideAppliedFromDriver] 🚀 Starting logic...'); + await getUpdatedRideForDriverApply(rideId); + + if (['Speed', 'Awfar Car'].contains(box.read(BoxName.carType))) { + NotificationController().showNotification('Fixed Price'.tr, + 'The captain is responsible for the route.'.tr, 'ding'); + } else if (['Comfort', 'Lady'].contains(box.read(BoxName.carType))) { + NotificationController().showNotification('Attention'.tr, + 'The price may increase if the route changes.'.tr, 'ding'); + } + + isApplied = true; + statusRide = 'Apply'; + rideConfirm = false; + isSearchingWindow = false; + _isDriverAppliedLogicExecuted = true; + + update(); + + // إيقاف جلب السيارات المجاورة ومسحها، باستثناء السائق الذي قبل الطلب + mapEngine.reloadStartApp = false; + mapEngine.markers.removeWhere((marker) => marker.markerId.value != driverId.toString()); + mapEngine.update(); + + await getDriverCarsLocationToPassengerAfterApplied(); + + if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) { + LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last; + Log.print( + '[rideAppliedFromDriver] 📍 Driver at: $driverPos, Passenger at: $passengerLocation'); + await getInitialDriverDistanceAndDuration(driverPos, passengerLocation); + await drawDriverPathOnly(driverPos, passengerLocation); + mapEngine.fitCameraToPoints(driverPos, passengerLocation); + } + + startTimerFromDriverToPassengerAfterApplied(); + } + + Future getInitialDriverDistanceAndDuration( + LatLng driverPos, LatLng passengerPos) async { + final String apiUrl = 'https://routec.intaleq.xyz/route'; + final String apiKey = Env.mapKeyOsm; + final String origin = '${driverPos.latitude},${driverPos.longitude}'; + final String dest = '${passengerPos.latitude},${passengerPos.longitude}'; + + final Uri uri = Uri.parse( + '$apiUrl?origin=$origin&destination=$dest&steps=false&overview=false'); + + try { + final response = await http.get(uri, headers: {'X-API-KEY': apiKey}); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data['status'] == 'ok') { + double durationSecondsRaw = (data['duration_s'] as num).toDouble(); + int finalDurationSeconds = + (durationSecondsRaw * kDurationScalar).toInt(); + double distanceMeters = (data['distance_m'] as num).toDouble(); + + updateDriverRouteMetrics( + etaSeconds: finalDurationSeconds, + distanceMeters: distanceMeters, + ); + + update(); + } + } + } catch (e) { + Log.print('Error getInitialDriverDistanceAndDuration: $e'); + } + } + + int durationToPassenger = 0; + String distanceByPassenger = ''; + + Future drawDriverPathOnly(LatLng driverPos, LatLng passengerPos) async { + final String apiUrl = 'https://routec.intaleq.xyz/route'; + final String apiKey = Env.mapKeyOsm; + final String origin = '${driverPos.latitude},${driverPos.longitude}'; + final String dest = '${passengerPos.latitude},${passengerPos.longitude}'; + + final Uri uri = Uri.parse( + '$apiUrl?origin=$origin&destination=$dest&steps=false&overview=full'); + + try { + final response = await http.get(uri, headers: {'X-API-KEY': apiKey}); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data['status'] == 'ok' && data['polyline'] != null) { + final String pointsString = data['polyline']; + List decodedPoints = + await compute(decodePolylineIsolate, pointsString); + _currentDriverRoutePoints = decodedPoints; + final double decodedDistance = _pathDistanceMeters(decodedPoints); + if (decodedDistance > 0) { + _currentDriverRouteDistanceMeters = decodedDistance; + } + + polyLines = polyLines + .where((p) => + !p.polylineId.value.startsWith('driver_route') && + p.polylineId.value != 'main_route' && + p.polylineId.value != 'route_primary' && + p.polylineId.value != 'route_direct') + .toSet(); + + // رسم خط صلب (Solid) من السائق للراكب + final Polyline driverSolidPolyline = Polyline( + polylineId: const PolylineId('driver_route_solid'), + points: decodedPoints, + color: Colors.amber, // مسار القدوم باللون الأصفر + width: 5, + ); + polyLines.add(driverSolidPolyline); + update(); + } + } + } catch (e) { + Log.print('Error drawing driver path: $e'); + } + } + + void listenToRideStatusStream() { + rideStatusStream.listen((rideStatus) { + Log.print("Ride Status: $rideStatus"); + }, onError: (error) { + Log.print("Error in Ride Status Stream: $error"); + }); + } + + void start15SecondTimer(String rideId) {} + + void startUiCountdown() { + _uiCountdownTimer?.cancel(); + progress = 0; + remainingTime = durationTimer; + + _uiCountdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + int i = timer.tick; + progress = i / durationTimer; + remainingTime = durationTimer - i; + + if (remainingTime <= 0) { + timer.cancel(); + rideConfirm = false; + timeToPassengerFromDriverAfterApplied += durationToPassenger; + timerEnded(); + } + update(); + }); + } + + double progress = 0; + int durationTimer = 9; + + void timerEnded() async { + runEvery30SecondsUntilConditionMet(); + isCancelRidePageShown = false; + update(); + } + + late String driverCarModel = '', driverCarMake = '', driverLicensePlate = ''; + + Future getUpdatedRideForDriverApply(String rideId) async { + if (rideId == 'yet' || rideId.isEmpty) return; + + try { + final res = await CRUD().get( + link: "${AppLink.server}/ride/rides/getRideOrderID.php", + payload: {'passengerID': box.read(BoxName.passengerID).toString()}); + + if (res != 'failure') { + var response = jsonDecode(res); + Log.print('getUpdatedRideForDriverApply Response: $response'); + + if (response['status'] == 'success' && + response['data'] != null && + response['data'] is Map) { + var data = response['data']; + + driverId = data['driver_id']?.toString() ?? ''; + driverPhone = data['phone']?.toString() ?? ''; + driverCarMake = data['make']?.toString() ?? ''; + model = data['model']?.toString() ?? ''; + colorHex = data['color_hex']?.toString() ?? ''; + carColor = data['color']?.toString() ?? ''; + make = data['make']?.toString() ?? ''; + licensePlate = data['car_plate']?.toString() ?? ''; + + String firstName = data['passengerName']?.toString() ?? ''; + String lastName = data['last_name']?.toString() ?? ''; + passengerName = + lastName.isNotEmpty ? "$firstName $lastName" : firstName; + driverName = data['driverName']?.toString() ?? ''; + driverToken = data['token']?.toString() ?? ''; + carYear = data['year']?.toString() ?? ''; + driverRate = data['ratingDriver']?.toString() ?? '5.0'; + + update(); + } + } + } catch (e) { + Log.print("Error in getUpdatedRideForDriverApply: $e"); + } + } + + String getLocationArea(double latitude, double longitude) { + LatLng passengerPoint = LatLng(latitude, longitude); + String previousCountry = box.read(BoxName.countryCode)?.toString() ?? ''; + String newCountry = 'Jordan'; + + if (isPointInPolygon(passengerPoint, CountryPolygons.jordanBoundary)) { + newCountry = 'Jordan'; + } else if (isPointInPolygon( + passengerPoint, CountryPolygons.syriaBoundary)) { + newCountry = 'Syria'; + } else if (isPointInPolygon( + passengerPoint, CountryPolygons.egyptBoundary)) { + newCountry = 'Egypt'; + } else { + newCountry = 'Jordan'; + } + + box.write(BoxName.countryCode, newCountry); + box.write(BoxName.serverChosen, AppLink.server); + + if (newCountry != previousCountry) { + unawaited(getKazanPercent()); + } + + return newCountry; + } + + bool isPointInPolygon(LatLng point, List polygon) { + int intersections = 0; + for (int i = 0; i < polygon.length; i++) { + LatLng vertex1 = polygon[i]; + LatLng vertex2 = polygon[(i + 1) % polygon.length]; + + if (_rayIntersectsSegment(point, vertex1, vertex2)) { + intersections++; + } + } + return intersections % 2 != 0; + } + + bool _rayIntersectsSegment(LatLng point, LatLng vertex1, LatLng vertex2) { + double px = point.longitude; + double py = point.latitude; + double v1x = vertex1.longitude; + double v1y = vertex1.latitude; + double v2x = vertex2.longitude; + double v2y = vertex2.latitude; + + if ((py < v1y && py < v2y) || (py > v1y && py > v2y)) { + return false; + } + double intersectX = v1x + (py - v1y) * (v2x - v1x) / (v2y - v1y); + return intersectX > px; + } + + String generateTrackingLink(String rideId, String driverId) { + String cleanRideId = rideId.toString().trim(); + String cleanDriverId = driverId.toString().trim(); + const String secretSalt = "Intaleq_Secure_Track_2025"; + + String rawString = "$cleanRideId$cleanDriverId$secretSalt"; + var bytes = utf8.encode(rawString); + var digest = md5.convert(bytes); + String token = digest.toString(); + + return "https://intaleqapp.com/track/index.php?id=$cleanRideId&token=$token"; + } + + calcualateDistsanceInMetet(LatLng prev, current) async { + double distance2 = Geolocator.distanceBetween( + prev.latitude, + prev.longitude, + current.latitude, + current.longitude, + ); + return distance2; + } + + uploadPassengerLocation() async { + await CRUD().post(link: AppLink.addpassengerLocation, payload: { + "passengerId": box.read(BoxName.passengerID), + "lat": passengerLocation.latitude.toString(), + "lng": passengerLocation.longitude.toString(), + "rideId": rideId.toString() + }); + } + + void _showRideStartNotifications() { + if (['Speed', 'Awfar Car'].contains(box.read(BoxName.carType))) { + NotificationController().showNotification('Fixed Price'.tr, + 'The captain is responsible for the route.'.tr, 'ding'); + } else if (['Comfort', 'Lady'].contains(box.read(BoxName.carType))) { + NotificationController().showNotification('Attention'.tr, + 'The price may increase if the route changes.'.tr, 'ding'); + } + } + + bool promoTaken = false; + final promo = TextEditingController(); + + void applyPromoCodeToPassenger(BuildContext context) async { + if (promoTaken == true) { + MyDialog().getDialog( + 'Promo Already Used'.tr, + 'You have already used this promo code.'.tr, + () => Get.back(), + ); + return; + } + + if (!promoFormKey.currentState!.validate()) return; + + const double minPromoLowSYP = 172; + const double minPromoHighSYP = 200; + + try { + final value = await CRUD().get( + link: AppLink.getPassengersPromo, + payload: {'promo_code': promo.text}, + ); + + if (value == 'failure') { + MyDialog().getDialog( + 'Promo Ended'.tr, + 'The promotion period has ended.'.tr, + () => Get.back(), + ); + return; + } + + final bool eligibleNow = (totalPassengerSpeed >= minPromoLowSYP) || + (totalPassengerBalash >= minPromoLowSYP) || + (totalPassengerComfort >= minPromoHighSYP) || + (totalPassengerElectric >= minPromoHighSYP) || + (totalPassengerLady >= minPromoHighSYP); + + if (!eligibleNow) { + Get.snackbar( + 'Lowest Price Achieved'.tr, + 'Cannot apply further discounts.'.tr, + backgroundColor: AppColor.yellowColor, + ); + return; + } + + final decode = jsonDecode(value); + if (decode["status"] != "success") { + MyDialog().getDialog( + 'Promo Ended'.tr, + 'The promotion period has ended.'.tr, + () => Get.back(), + ); + return; + } + + Get.snackbar('Promo Code Accepted'.tr, '', + backgroundColor: AppColor.greenColor); + + final firstElement = decode["message"][0]; + final int discountPercentage = + int.tryParse(firstElement['amount'].toString()) ?? 0; + + final double walletVal = double.tryParse( + box.read(BoxName.passengerWalletTotal)?.toString() ?? '0') ?? + 0.0; + + final bool isWalletNegative = walletVal < 0; + + double _applyDiscountPerTier({ + required double fare, + required double minThreshold, + required bool isWalletNegative, + }) { + if (fare < minThreshold) return fare; + + final double discount = fare * (discountPercentage / 100.0); + double result; + + if (isWalletNegative) { + double neg = (-1) * walletVal; + result = fare + neg - discount; + } else { + result = fare - discount; + } + + if (result < minThreshold) { + result = minThreshold; + } + return result.clamp(0.0, double.infinity); + } + + totalPassengerComfort = _applyDiscountPerTier( + fare: totalPassengerComfort, + minThreshold: minPromoHighSYP, + isWalletNegative: isWalletNegative, + ); + + totalPassengerElectric = _applyDiscountPerTier( + fare: totalPassengerElectric, + minThreshold: minPromoHighSYP, + isWalletNegative: isWalletNegative, + ); + + totalPassengerLady = _applyDiscountPerTier( + fare: totalPassengerLady, + minThreshold: minPromoHighSYP, + isWalletNegative: isWalletNegative, + ); + + totalPassengerSpeed = _applyDiscountPerTier( + fare: totalPassengerSpeed, + minThreshold: minPromoLowSYP, + isWalletNegative: isWalletNegative, + ); + + totalPassengerBalash = _applyDiscountPerTier( + fare: totalPassengerBalash, + minThreshold: minPromoLowSYP, + isWalletNegative: isWalletNegative, + ); + + totalDriver = totalDriver - (totalDriver * discountPercentage / 100.0); + promoTaken = true; + update(); + + Confetti.launch( + context, + options: const ConfettiOptions(particleCount: 100, spread: 70, y: 0.6), + ); + + Get.back(); + await Future.delayed(const Duration(milliseconds: 120)); + } catch (e) { + Get.snackbar('Error'.tr, e.toString(), + backgroundColor: AppColor.redColor); + } + } + + double getDistanceFromText(String distanceText) { + String distanceValue = distanceText.replaceAll(RegExp(r'[^0-9.]+'), ''); + double distance = double.parse(distanceValue); + return distance; + } + + double costForDriver = 0; + + Future bottomSheet() async { + const double minFareSYP = 160; + const double minBillableKm = 0.3; + const double ladyFlatAddon = 20; + const double airportAddonSYP = 200; + const double damascusAirportBoundAddon = 1400; + + const double electricPerKmUplift = 4; + const double electricFlatAddon = 10; + + const double longSpeedThresholdKm = 40.0; + const double longSpeedPerKm = 26.0; + + const double mediumDistThresholdKm = 25.0; + const double longDistThresholdKm = 35.0; + const double longTripPerMin = 6.0; + const int minuteCapMedium = 60; + const int minuteCapLong = 80; + const int freeMinutesLong = 10; + + const double extraReduction100 = 0.07; + const double maxReductionCap = 0.35; + + durationToAdd = Duration(seconds: durationToRide); + hours = durationToAdd.inHours; + minutes = (durationToAdd.inMinutes % 60).round(); + final DateTime currentTime = DateTime.now(); + newTime = currentTime.add(durationToAdd); + averageDuration = (durationToRide / 60) / distance; + final int waypointSurchargeMinutes = activeMenuWaypointCount * 5; + final int totalMinutes = + (durationToRide / 60).floor() + waypointSurchargeMinutes; + + bool _isAirport(String s) { + final t = s.toLowerCase(); + return t.contains('airport') || + s.contains('مطار') || + s.contains('المطار'); + } + + bool _isClub(String s) { + final t = s.toLowerCase(); + return t.contains('club') || + t.contains('nightclub') || + t.contains('night club') || + s.contains('ديسكو') || + s.contains('ملهى ليلي'); + } + + bool _isInsideDamascusAirportBounds(double lat, double lng) { + final double northLat = 33.415313; + final double southLat = 33.400265; + final double eastLng = 36.531505; + final double westLng = 36.499687; + + bool isLatInside = (lat <= northLat) && (lat >= southLat); + bool isLngInside = (lng <= eastLng) && (lng >= westLng); + return isLatInside && isLngInside; + } + + final double naturePerMin = naturePrice; + final double latePerMin = latePrice; + final double heavyPerMin = heavyPrice; + + double _perMinuteByTime(DateTime now, bool clubCtx) { + final h = now.hour; + if (h >= 21 || h < 1) return latePerMin; + if (h >= 1 && h < 5) return clubCtx ? (latePerMin * 2) : latePerMin; + if (h >= 14 && h <= 17) return heavyPerMin; + return naturePerMin; + } + + double _applyMinFare(double fare) => + (fare < minFareSYP) ? minFareSYP : fare; + + double _withCommission(double base) => + (base * (1 + kazan / 100)).ceilToDouble(); + + final bool airportCtx = + _isAirport(startNameAddress) || _isAirport(endNameAddress); + final bool clubCtx = _isClub(startNameAddress) || _isClub(endNameAddress); + + double destLat = 0.0; + double destLng = 0.0; + try { + destLat = myDestination.latitude; + destLng = myDestination.longitude; + } catch (_) { + if (locSearch.coordinatesWithoutEmpty.isNotEmpty) { + destLat = double.tryParse( + locSearch.coordinatesWithoutEmpty.last.split(',')[0]) ?? + 0.0; + destLng = double.tryParse( + locSearch.coordinatesWithoutEmpty.last.split(',')[1]) ?? + 0.0; + } + } + + final bool damascusAirportBoundCtx = + _isInsideDamascusAirportBounds(destLat, destLng); + final bool isInDamascusAirportBoundCtx = _isInsideDamascusAirportBounds( + newMyLocation.latitude.toDouble(), + newMyLocation.longitude.toDouble(), + ); + + final double billableDistance = + (distance < minBillableKm) ? minBillableKm : distance; + + final bool isLongSpeed = billableDistance > longSpeedThresholdKm; + final double perKmSpeedBaseFromServer = speedPrice; + final double perKmSpeed = + isLongSpeed ? longSpeedPerKm : perKmSpeedBaseFromServer; + + double reductionPct40 = 0.0; + if (perKmSpeedBaseFromServer > 0) { + reductionPct40 = (1.0 - (longSpeedPerKm / perKmSpeedBaseFromServer)) + .clamp(0.0, maxReductionCap); + } + final double reductionPct100 = + (reductionPct40 + extraReduction100).clamp(0.0, maxReductionCap); + double distanceReduction = 0.0; + if (billableDistance > 100.0) { + distanceReduction = reductionPct100; + } else if (billableDistance > 40.0) { + distanceReduction = reductionPct40; + } + + double effectivePerMin = _perMinuteByTime(currentTime, clubCtx); + int billableMinutes = totalMinutes; + if (billableDistance > longDistThresholdKm) { + effectivePerMin = longTripPerMin; + final int capped = + (billableMinutes > minuteCapLong) ? minuteCapLong : billableMinutes; + billableMinutes = capped - freeMinutesLong; + if (billableMinutes < 0) billableMinutes = 0; + } else if (billableDistance > mediumDistThresholdKm) { + effectivePerMin = longTripPerMin; + billableMinutes = (billableMinutes > minuteCapMedium) + ? minuteCapMedium + : billableMinutes; + } + + final double perKmComfortRaw = comfortPrice; + final double perKmDelivery = deliveryPrice; + final double perKmVanRaw = + (familyPrice > 0 ? familyPrice : (speedPrice + 13)); + final double perKmElectricRaw = perKmComfortRaw + electricPerKmUplift; + + double perKmComfort = perKmComfortRaw * (1.0 - distanceReduction); + double perKmElectric = perKmElectricRaw * (1.0 - distanceReduction); + double perKmVan = perKmVanRaw * (1.0 - distanceReduction); + perKmComfort = perKmComfort.clamp(0, double.infinity); + perKmElectric = perKmElectric.clamp(0, double.infinity); + perKmVan = perKmVan.clamp(0, double.infinity); + final double perKmBalash = (perKmSpeed - 5).clamp(0, double.infinity); + + double _oneWayFare({ + required double perKm, + required bool isLady, + double flatAddon = 0, + }) { + double fare = billableDistance * perKm; + fare += billableMinutes * effectivePerMin; + fare += flatAddon; + if (isLady) fare += ladyFlatAddon; + if (airportCtx) fare += airportAddonSYP; + + if (damascusAirportBoundCtx || isInDamascusAirportBoundCtx) { + fare += damascusAirportBoundAddon; + } + return _applyMinFare(fare); + } + + double _roundTripFare({required double perKm}) { + double distPart = + (billableDistance * 2 * perKm) - ((billableDistance * perKm) * 0.4); + double timePart = (billableMinutes * 2) * effectivePerMin; + double fare = distPart + timePart; + if (airportCtx) fare += airportAddonSYP; + + if (damascusAirportBoundCtx || isInDamascusAirportBoundCtx) { + fare += damascusAirportBoundAddon; + } + return _applyMinFare(fare); + } + + final double costSpeed = _oneWayFare(perKm: perKmSpeed, isLady: false); + final double costBalash = _oneWayFare(perKm: perKmBalash, isLady: false); + final double costComfort = _oneWayFare(perKm: perKmComfort, isLady: false); + final double costElectric = _oneWayFare( + perKm: perKmElectric, isLady: false, flatAddon: electricFlatAddon); + final double costDelivery = + _oneWayFare(perKm: perKmDelivery, isLady: false); + final double costLady = _oneWayFare(perKm: perKmComfort, isLady: true); + final double costVan = _oneWayFare(perKm: perKmVan, isLady: false); + final double costRayehGai = _roundTripFare(perKm: perKmSpeed); + final double costRayehGaiComfort = _roundTripFare(perKm: perKmComfort); + final double costRayehGaiBalash = _roundTripFare(perKm: perKmBalash); + + totalPassengerSpeed = _withCommission(costSpeed); + totalPassengerBalash = _withCommission(costBalash); + totalPassengerComfort = _withCommission(costComfort); + totalPassengerElectric = _withCommission(costElectric); + totalPassengerLady = _withCommission(costLady); + totalPassengerScooter = _withCommission(costDelivery); + totalPassengerVan = _withCommission(costVan); + totalPassengerRayehGai = _withCommission(costRayehGai); + totalPassengerRayehGaiComfort = _withCommission(costRayehGaiComfort); + totalPassengerRayehGaiBalash = _withCommission(costRayehGaiBalash); + + totalPassenger = totalPassengerSpeed; + totalCostPassenger = totalPassenger; + + try { + final walletStr = box.read(BoxName.passengerWalletTotal).toString(); + final walletVal = double.tryParse(walletStr) ?? 0.0; + if (walletVal < 0) { + final neg = (-1) * walletVal; + totalPassenger += neg; + totalPassengerComfort += neg; + totalPassengerElectric += neg; + totalPassengerLady += neg; + totalPassengerBalash += neg; + totalPassengerScooter += neg; + totalPassengerRayehGai += neg; + totalPassengerRayehGaiComfort += neg; + totalPassengerRayehGaiBalash += neg; + totalPassengerVan += neg; + } + } catch (e) { + Log.print("Error: $e"); + } + + update(); + mapEngine.changeBottomSheetShown(forceValue: true); + } + + // حساب المسار بين السائق والراكب وعرضه على الخريطة. + // ترسل هذه الدالة طلبًا للخادم للحصول على إحداثيات المسار وتفك تشفيره. + // ثم تقوم بتحديث المسافة والوقت وعرض الخطوط المناسبة على الخريطة. + Future calculateDriverToPassengerRoute( + LatLng driverPos, LatLng passengerPos, + {bool isBeginPhase = false}) async { + final Map queryParams = { + 'fromLat': driverPos.latitude.toString(), + 'fromLng': driverPos.longitude.toString(), + 'toLat': passengerPos.latitude.toString(), + 'toLng': passengerPos.longitude.toString(), + }; + final uri = + Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams); + + Log.print('📍 Calculating Driver Route: $uri'); + + try { + final response = await http.get(uri, headers: { + 'x-api-key': Env.mapSaasKey, + }).timeout(const Duration(seconds: 20)); + + if (response.statusCode == 200) { + final responseData = json.decode(response.body); + + var routeData = responseData['routes'] != null + ? responseData['routes'][0] + : responseData; + + double durationSecondsRaw = (routeData['duration'] as num).toDouble(); + int finalDurationSeconds = + (durationSecondsRaw * kDurationScalar).toInt(); + double distanceMeters = (routeData['distance'] as num).toDouble(); + + updateDriverRouteMetrics( + etaSeconds: finalDurationSeconds, + distanceMeters: distanceMeters, + ); + _currentDriverRouteDistanceMeters = distanceMeters; + _currentDriverRouteDurationSeconds = finalDurationSeconds; + + int minutes = (finalDurationSeconds / 60).floor(); + int seconds = finalDurationSeconds % 60; + stringRemainingTimeToPassenger = + '$minutes:${seconds.toString().padLeft(2, '0')}'; + + Log.print( + '✅ Driver Route Info: $minutes min, ${distanceMeters.toInt()} m'); + + String pointsString = + routeData['points'] ?? routeData['geometry'] ?? ""; + if (pointsString.isNotEmpty) { + List decodedPoints = + await compute(decodePolylineIsolate, pointsString); + _currentDriverRoutePoints = decodedPoints; + final double decodedDistance = _pathDistanceMeters(decodedPoints); + if (decodedDistance > 0) { + _currentDriverRouteDistanceMeters = decodedDistance; + } + // مسح كل السلمات السابقة (الخط المستمر والمتقطع على حد سواء) + polyLines = polyLines + .where((p) => + !p.polylineId.value.startsWith('driver_route') && + p.polylineId.value != 'main_route' && + p.polylineId.value != 'route_primary' && + p.polylineId.value != 'route_direct') + .toSet(); + + if (isBeginPhase) { + // حالة Begin: لا نرسم مسار السائق القديم إطلاقاً لأنه وصل والآن الرحلة ستبدأ + polyLines = polyLines + .where((p) => !p.polylineId.value.startsWith('driver_route')) + .toSet(); + } else { + // مسح السلمات القديمة أولاً + polyLines = polyLines + .where((p) => + !p.polylineId.value.startsWith('driver_route') && + p.polylineId.value != 'main_route' && + p.polylineId.value != 'route_primary' && + p.polylineId.value != 'route_direct') + .toSet(); + // حالة Apply/Arrived: خط متصل صلب بدل المتقطع + polyLines = { + ...polyLines, + Polyline( + polylineId: const PolylineId('driver_route_solid'), + points: decodedPoints, + color: Colors.amber, // مسار القدوم باللون الأصفر + width: 5, + ) + }; + } + } + + mapEngine.fitCameraToPoints(driverPos, passengerPos); + update(); + } + } catch (e) { + Log.print('❌ Error calculating driver route: $e'); + } + } + + Future checkAndRecalculateIfDeviated( + LatLng driverPos, { + double? heading, + double? speed, + }) async { + if (_isRecalculatingRoute || _currentDriverRoutePoints.isEmpty) return; + + double minDistance = 100000.0; + int closestIdx = 0; + for (int idx = 0; idx < _currentDriverRoutePoints.length; idx++) { + final point = _currentDriverRoutePoints[idx]; + double dist = Geolocator.distanceBetween(driverPos.latitude, + driverPos.longitude, point.latitude, point.longitude); + if (dist < minDistance) { + minDistance = dist; + closestIdx = idx; + } + } + + final bool distanceDeviation = minDistance > _deviationThresholdMeters; + final bool headingDeviation = _isHeadingAwayFromRoute( + heading: heading, + speed: speed, + closestRouteIndex: closestIdx, + distanceFromRouteMeters: minDistance, + ); + + if (!headingDeviation) { + _routeHeadingMismatchCount = 0; + } else { + _routeHeadingMismatchCount++; + } + + if (distanceDeviation || _routeHeadingMismatchCount >= 2) { + Log.print( + "⚠️ Driver deviated (${minDistance.toStringAsFixed(1)} m, heading mismatch: $_routeHeadingMismatchCount). Recalculating route..."); + _routeHeadingMismatchCount = 0; + _isRecalculatingRoute = true; + if (statusRide == 'Begin' || + currentRideState.value == RideState.inProgress) { + await calculateDriverToPassengerRoute(driverPos, myDestination, + isBeginPhase: true); + } else { + await calculateDriverToPassengerRoute(driverPos, passengerLocation); + } + _isRecalculatingRoute = false; + } + } + + // تحديث الجزء المتبقي من المسار والمسافة والوقت بشكل تفاعلي. + // تحدد الدالة أقرب نقطة للسائق على المسار الحالي وتقوم بقص النقاط السابقة. + // ثم تعيد حساب المسافة والوقت المتبقيين محلياً وتحديث الخطوط على الخريطة. + void updateRemainingRoute(LatLng driverPos, {bool updateEta = true}) { + if (_currentDriverRoutePoints.isEmpty) return; + + int closestIdx = 0; + double minDistance = double.infinity; + for (int i = 0; i < _currentDriverRoutePoints.length; i++) { + double dist = Geolocator.distanceBetween( + driverPos.latitude, + driverPos.longitude, + _currentDriverRoutePoints[i].latitude, + _currentDriverRoutePoints[i].longitude); + if (dist < minDistance) { + minDistance = dist; + closestIdx = i; + } + } + + if (minDistance < 150.0) { + List remainingPoints = + _currentDriverRoutePoints.sublist(closestIdx); + + if (updateEta) { + final double remainingDistance = _pathDistanceMeters(remainingPoints); + int remainingDuration = _currentDriverRouteDurationSeconds; + if (remainingDistance > 0 && + _currentDriverRouteDistanceMeters > 0 && + _currentDriverRouteDurationSeconds > 0) { + remainingDuration = + ((_currentDriverRouteDurationSeconds * remainingDistance) / + _currentDriverRouteDistanceMeters) + .round(); + } + + remainingDuration = max(0, remainingDuration); + updateDriverRouteMetrics( + etaSeconds: remainingDuration, + distanceMeters: remainingDistance, + ); + } + + polyLines = polyLines + .where((p) => + !p.polylineId.value.startsWith('driver_route') && + p.polylineId.value != 'main_route' && + p.polylineId.value != 'route_primary' && + p.polylineId.value != 'route_direct') + .toSet(); + + if (statusRide == 'Begin' || + currentRideState.value == RideState.inProgress) { + // لا نرسم أي شيء في حالة البدء لأنه وصل + polyLines = polyLines + .where((p) => !p.polylineId.value.startsWith('driver_route')) + .toSet(); + } else { + polyLines = { + ...polyLines, + Polyline( + polylineId: const PolylineId('driver_route_solid'), + points: remainingPoints, + color: Colors.amber, + width: 5, + ), + }; + } + update(); + } + } + + Future getDriverCarsLocationToPassengerAfterApplied() async { + bool isRideActive = (statusRide == 'Apply' || + statusRide == 'Arrived' || + statusRide == 'Begin' || + currentRideState.value == RideState.driverApplied || + currentRideState.value == RideState.driverArrived || + currentRideState.value == RideState.inProgress); + + if (!isRideActive || + statusRide == 'Finished' || + statusRide == 'Cancel' || + currentRideState.value == RideState.finished || + currentRideState.value == RideState.noRide || + currentRideState.value == RideState.preCheckReview) { + return; + } + + if (_isFetchingDriverLocation) return; + _isFetchingDriverLocation = true; + + try { + var res = await CRUD().get( + link: AppLink.getDriverCarsLocationToPassengerAfterApplied, + payload: {'driver_id': driverId}); + + if (res != 'failure') { + var datadriverLocation = jsonDecode(res); + + if (datadriverLocation['message'] != null && + datadriverLocation['message'].isNotEmpty) { + var _data = datadriverLocation['message'][0]; + + LatLng newDriverPos = LatLng( + double.parse(_data['latitude'].toString()), + double.parse(_data['longitude'].toString())); + double newHeading = + double.tryParse(_data['heading']?.toString() ?? '0') ?? 0.0; + double speed = + double.tryParse(_data['speed']?.toString() ?? '0') ?? 0; + + if (driverCarsLocationToPassengerAfterApplied.length > 10) { + driverCarsLocationToPassengerAfterApplied.removeAt(0); + } + driverCarsLocationToPassengerAfterApplied.add(newDriverPos); + checkAndRecalculateIfDeviated( + newDriverPos, + heading: newHeading, + speed: speed, + ); + updateRemainingRoute(newDriverPos); + if (statusRide == 'Begin' || + currentRideState.value == RideState.inProgress) { + double zoom = 16.5; + if (speed > 0) { + zoom = 17.0 - ((speed - 10) / 70) * 2.5; + zoom = zoom.clamp(14.5, 17.0); + } + if (mapEngine.mapController != null) { + mapEngine.mapController!.animateCamera( + CameraUpdate.newLatLngZoom(newDriverPos, zoom)); + } + } + mapEngine.clearMarkersExceptStartEndAndDriver(); + reloadMarkerDriverCarsLocationToPassengerAfterApplied( + datadriverLocation); + } + } + update(); + } catch (e) { + Log.print('Error fetching driver location: $e'); + } finally { + _isFetchingDriverLocation = false; + } + } + + void reloadMarkerDriverCarsLocationToPassengerAfterApplied( + dynamic datadriverLocation) { + if (datadriverLocation == null || + datadriverLocation['message'] == null || + datadriverLocation['message'].isEmpty) { + return; + } + + var driverData = datadriverLocation['message'][0]; + LatLng newPosition = LatLng(double.parse(driverData['latitude'].toString()), + double.parse(driverData['longitude'].toString())); + double newHeading = + double.tryParse(driverData['heading'].toString()) ?? 0.0; + + String icon; + if (driverData['model'].toString().contains('دراجة') || + driverData['make'].toString().contains('دراجة')) { + icon = mapEngine.motoIcon; + } else if (driverData['gender'] == 'Female') { + icon = mapEngine.ladyIcon; + } else { + icon = mapEngine.carIcon; + } + + final String markerId = 'assigned_driver_marker'; + final mId = MarkerId(markerId); + final existingMarker = markers.cast().firstWhere( + (m) => m?.markerId == mId, + orElse: () => null, + ); + + if (existingMarker != null) { + mapEngine.smoothlyUpdateMarker( + existingMarker, newPosition, newHeading, icon); + } else { + markers = { + ...markers, + Marker( + markerId: mId, + position: newPosition, + rotation: newHeading, + icon: InlqBitmap.fromStyleImage(icon), + anchor: const Offset(0.5, 0.5), + ), + }; + update(); + } + } + + void updateDriverMarker(LatLng position, double heading) { + const String markerId = 'assigned_driver_marker'; + const mId = MarkerId(markerId); + + // Choose icon based on vehicle type + String icon; + if (model.contains('دراجة') || make.contains('دراجة')) { + icon = mapEngine.motoIcon; + } else { + icon = mapEngine.carIcon; + } + + final existingMarker = markers.cast().firstWhere( + (m) => m?.markerId == mId, + orElse: () => null, + ); + + if (existingMarker != null) { + mapEngine.smoothlyUpdateMarker(existingMarker, position, heading, icon); + } else { + markers = { + ...markers, + Marker( + markerId: mId, + position: position, + icon: InlqBitmap.fromStyleImage(icon), + rotation: heading, + anchor: const Offset(0.5, 0.5), + ), + }; + update(); + } + } + + Future runEvery30SecondsUntilConditionMet() async { + double tripDurationInMinutes = durationToPassenger / 5; + int loopCount = tripDurationInMinutes.ceil(); + for (var i = 0; i < loopCount; i++) { + await Future.delayed(const Duration(seconds: 5)); + if (rideTimerBegin == true || statusRide == 'Apply') { + await getDriverCarsLocationToPassengerAfterApplied(); + } + } + } + + Future runWhenRideIsBegin() async { + double tripDurationInMinutes = durationToRide / 6; + int loopCount = tripDurationInMinutes.ceil(); + mapEngine.clearMarkersExceptStartEndAndDriver(); + for (var i = 0; i < loopCount; i++) { + await Future.delayed(const Duration(seconds: 4)); + await getDriverCarsLocationToPassengerAfterApplied(); + } + } + + // بدء مراقب اتصال المقبس (Socket Watchdog). + // يتحقق دورياً كل 5 ثوانٍ من آخر تحديث تم استلامه عبر المقبس. + // في حال وجود خمول لأكثر من 15 ثانية، يجلب الموقع عبر واجهة التطبيق كطلب مفرد. + // وإذا زاد الخمول عن 30 ثانية، يبدأ آلية الاقتراع الدوري كخيار احتياطي. + void _startSocketWatchdog() { + _watchdogTimer?.cancel(); + Log.print("👀 Starting Socket Watchdog (Hybrid Mode)..."); + + _watchdogTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { + if (currentRideState.value != RideState.driverApplied && + currentRideState.value != RideState.driverArrived && + currentRideState.value != RideState.inProgress) { + timer.cancel(); + return; + } + + final lastTime = mapSocket.lastDriverLocationTime ?? + DateTime.now().subtract(const Duration(minutes: 1)); + final difference = DateTime.now().difference(lastTime).inSeconds; + + if (difference < 15 && mapSocket.isSocketConnected) { + if (_locationPollingTimer != null && + _rideAcceptedViaSource == "Socket") { + Log.print("✅ Socket recovered. Stopping polling fallback."); + stopDriverLocationPolling(); + } + } else if (difference >= 15 && difference < 30) { + Log.print("⚠️ Socket silent for ${difference}s. Single API Poll..."); + await getDriverCarsLocationToPassengerAfterApplied(); + try { + String statusFromServer = await getRideStatus(rideId); + _handleServerStatusTransition(statusFromServer); + } catch (e) { + Log.print("Error polling ride status in watchdog: $e"); + } + } else if (difference >= 30) { + if (_locationPollingTimer == null) { + Log.print( + "🔴 Socket dead for ${difference}s. Activating polling fallback!"); + _startDriverLocationPollingWithTimer(); + } else { + await getDriverCarsLocationToPassengerAfterApplied(); + try { + String statusFromServer = await getRideStatus(rideId); + _handleServerStatusTransition(statusFromServer); + } catch (e) { + Log.print("Error polling ride status in watchdog: $e"); + } + } + } + }); + } + + // بدء الاقتراع الدوري لموقع السائق كخيار احتياطي عند توقف أو فشل المقبس. + // يقوم هذا التوقيت دورياً كل 6 ثوانٍ بجلب موقع السائق وتحديث حالة الرحلة. + void _startDriverLocationPollingWithTimer() { + Log.print("📍 Starting Driver Location Polling (6s interval)"); + _locationPollingTimer?.cancel(); + + _locationPollingTimer = Timer.periodic(Duration(seconds: 6), (timer) async { + if (currentRideState.value == RideState.finished || + currentRideState.value == RideState.cancelled || + currentRideState.value == RideState.noRide) { + timer.cancel(); + return; + } + await getDriverCarsLocationToPassengerAfterApplied(); + try { + String statusFromServer = await getRideStatus(rideId); + _handleServerStatusTransition(statusFromServer); + } catch (e) { + Log.print("Error polling ride status in fallback timer: $e"); + } + }); + } + + void _handleServerStatusTransition(String status) { + String lowerStatus = status.toLowerCase(); + Log.print( + "🔄 _handleServerStatusTransition status: $lowerStatus | Current state: ${currentRideState.value}"); + + if (lowerStatus == 'arrived' && + currentRideState.value != RideState.driverArrived) { + processDriverArrival("Polling"); + } else if ((lowerStatus == 'begin' || + lowerStatus == 'started' || + lowerStatus == 'inprogress') && + currentRideState.value != RideState.inProgress) { + processRideBegin(source: "Polling"); + } else if ((lowerStatus == 'finished' || lowerStatus == 'ended') && + currentRideState.value != RideState.finished && + currentRideState.value != RideState.preCheckReview) { + Log.print( + "🏁 Polling detected Finished. Releasing ride and moving to rating."); + stopAllTimers(); + currentRideState.value = RideState.preCheckReview; + tripFinishedFromDriver(); + _checkLastRideForReview(); + } else if (lowerStatus == 'cancelled' || lowerStatus == 'cancel') { + processRideCancelledByDriver({'reason': 'Cancelled by driver'}, + source: "Polling"); + } + } + + void stopDriverLocationPolling() { + Log.print("🛑 Stopping Location Polling"); + _locationPollingTimer?.cancel(); + _locationPollingTimer = null; + } + + Future _addRideToWaitingTable() async { + try { + LatLng startLoc = mapEngine.polylineCoordinates.first; + LatLng endLoc = mapEngine.polylineCoordinates.last; + await CRUD().post(link: AppLink.addWaitingRide, payload: { + 'id': rideId.toString(), + "start_location": '${startLoc.latitude},${startLoc.longitude}', + "end_location": '${endLoc.latitude},${endLoc.longitude}', + "date": DateTime.now().toString(), + "time": DateTime.now().toString(), + "price": totalPassenger.toStringAsFixed(2), + 'passenger_id': box.read(BoxName.passengerID).toString(), + 'status': 'waiting', + 'carType': box.read(BoxName.carType), + 'passengerRate': passengerRate.toStringAsFixed(2), + 'price_for_passenger': totalME.toStringAsFixed(2), + 'distance': distance.toStringAsFixed(1), + 'duration': duration.toStringAsFixed(1), + 'payment_method': + Get.find().isWalletChecked ? 'wallet' : 'cash', + "passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(), + }); + Log.print('[WaitingTable] Ride $rideId added to waiting_ride table.'); + } catch (e) { + Log.print('Error adding ride to waiting_ride table: $e'); + } + } + + double totalME = 0; + double passengerRate = 5; + double comfortPrice = 45; + double speedPrice = 40; + double mashwariPrice = 40; + double familyPrice = 55; + double deliveryPrice = 1.2; + + Future getKazanPercent() async { + var res = await CRUD().get( + link: AppLink.getKazanPercent, + payload: {'country': box.read(BoxName.countryCode).toString()}, + ); + if (res != 'failure') { + var json = jsonDecode(res); + var dataList = json['data'] ?? json['message']; + + if (dataList != null && dataList is List && dataList.isNotEmpty) { + var firstRow = dataList[0]; + kazan = double.parse(firstRow['kazan'].toString()); + naturePrice = double.parse(firstRow['naturePrice'].toString()); + heavyPrice = double.parse(firstRow['heavyPrice'].toString()); + latePrice = double.parse(firstRow['latePrice'].toString()); + comfortPrice = double.parse(firstRow['comfortPrice'].toString()); + speedPrice = double.parse(firstRow['speedPrice'].toString()); + deliveryPrice = double.parse(firstRow['deliveryPrice'].toString()); + mashwariPrice = double.parse(firstRow['freePrice'].toString()); + familyPrice = double.parse(firstRow['familyPrice'].toString()); + fuelPrice = double.parse(firstRow['fuelPrice'].toString()); + } + } + } + + Future getPassengerRate() async { + var res = await CRUD().get( + link: AppLink.getPassengerRate, + payload: {'passenger_id': box.read(BoxName.passengerID)}); + if (res != 'failure') { + var json = jsonDecode(res); + var message = json['data'] ?? json['message']; + if (message['rating'] == null) { + passengerRate = 5.0; + } else { + var rating = message['rating']; + if (rating is String) { + passengerRate = double.tryParse(rating) ?? 5.0; + } else if (rating is num) { + passengerRate = rating.toDouble(); + } else { + passengerRate = 5.0; + } + } + } else { + passengerRate = 5.0; + } + } + + Future addFingerPrint() async { + String fingerPrint = await DeviceHelper.getDeviceFingerprint(); + await CRUD().postWallet(link: AppLink.addFingerPrint, payload: { + 'token': (box.read(BoxName.tokenFCM.toString())), + 'passengerID': box.read(BoxName.passengerID).toString(), + "fingerPrint": fingerPrint + }); + } + + Future firstTimeRunToGetCoupon() async { + if (box.read(BoxName.isFirstTime).toString() == '0' && + box.read(BoxName.isInstall).toString() == '1' && + box.read(BoxName.isGiftToken).toString() == '0') { + var promoCode, discount, validity; + var resPromo = await CRUD().get(link: AppLink.getPromoFirst, payload: { + "passengerID": box.read(BoxName.passengerID).toString(), + }); + if (resPromo != 'failure') { + var d1 = jsonDecode(resPromo); + promoCode = d1['message']['promo_code']; + discount = d1['message']['amount']; + validity = d1['message']['validity_end_date']; + } + box.write(BoxName.isFirstTime, '1'); + + Get.dialog( + AlertDialog( + contentPadding: EdgeInsets.zero, + content: SizedBox( + width: 300, + child: PromoBanner( + promoCode: promoCode, + discountPercentage: discount, + validity: validity, + ), + ), + ), + ); + } + } + + Future detectAndCacheDeviceTier() async { + bool isHighEnd = await DevicePerformanceManager.isHighEndDevice(); + Log.print("Device Analysis - Is Flagship/HighEnd? $isHighEnd"); + box.write(BoxName.lowEndMode, !isHighEnd); + } + + Future initilizeGetStorage() async { + if (box.read(BoxName.addWork) == null) { + box.write(BoxName.addWork, 'addWork'); + } + if (box.read(BoxName.addHome) == null) { + box.write(BoxName.addHome, 'addHome'); + } + if (box.read(BoxName.lowEndMode) == null) { + detectAndCacheDeviceTier(); + } + } + + Future selectDriverAndCarForMishwariTrip() async { + double latitudeOffset = 0.1; + double longitudeOffset = 0.12; + + double southwestLat = passengerLocation.latitude - latitudeOffset; + double northeastLat = passengerLocation.latitude + latitudeOffset; + double southwestLon = passengerLocation.longitude - longitudeOffset; + double northeastLon = passengerLocation.longitude + longitudeOffset; + + var payload = { + 'southwestLat': southwestLat.toString(), + 'northeastLat': northeastLat.toString(), + 'southwestLon': southwestLon.toString(), + 'northeastLon': northeastLon.toString(), + }; + + try { + var res = await CRUD().get( + link: AppLink.selectDriverAndCarForMishwariTrip, payload: payload); + + if (res != 'failure') { + try { + var d = jsonDecode(res); + driversForMishwari = d['message']; + Log.print('driversForMishwari: $driversForMishwari'); + update(); + } catch (e) { + Log.print("Error decoding JSON: $e"); + } + } + } catch (e) { + Log.print("Error Mishwari select: $e"); + } + } + + List driversForMishwari = []; + final Rx selectedDateTime = DateTime.now().obs; + + void updateDateTime(DateTime newDateTime) { + selectedDateTime.value = newDateTime; + } + + Future mishwariOption() async { + isLoading = true; + update(); + await selectDriverAndCarForMishwariTrip(); + Future.delayed(Duration.zero); + isLoading = false; + update(); + Get.to(() => CupertinoDriverListWidget()); + } + + bool isLoading = false; + var driverIdVip = ''; + + Future saveTripData( + Map driver, DateTime tripDateTime) async { + try { + LatLng startLoc = mapEngine.polylineCoordinates.first; + Map tripData = { + 'id': driver['driver_id'].toString(), + 'phone': driver['phone'], + 'gender': driver['gender'], + 'name': driver['NAME'], + 'name_english': driver['name_english'], + 'address': driver['address'], + 'religion': driver['religion'] ?? 'UnKnown', + 'age': driver['age'].toString(), + 'education': driver['education'] ?? 'UnKnown', + 'license_type': driver['license_type'] ?? 'UnKnown', + 'national_number': driver['national_number'] ?? 'UnKnown', + 'car_plate': driver['car_plate'], + 'make': driver['make'], + 'model': driver['model'], + 'year': driver['year'].toString(), + 'color': driver['color'], + 'color_hex': driver['color_hex'], + 'displacement': driver['displacement'], + 'fuel': driver['fuel'], + 'token': driver['token'], + 'rating': driver['rating'].toString(), + 'countRide': driver['ride_count'].toString(), + 'passengerId': box.read(BoxName.passengerID), + 'timeSelected': tripDateTime.toIso8601String(), + 'status': 'pending', + 'startNameAddress': startNameAddress.toString(), + 'locationCoordinate': '${startLoc.latitude},${startLoc.longitude}', + }; + Log.print('tripData: $tripData'); + + var response = + await CRUD().post(link: AppLink.addMishwari, payload: tripData); + + if (response != 'failure') { + var id = response['message']['id'].toString(); + await CRUD() + .post(link: '${AppLink.server}/ride/rides/add.php', payload: { + "start_location": '${startLoc.latitude},${startLoc.longitude}', + "end_location": '${startLoc.latitude},${startLoc.longitude}', + "date": DateTime.now().toString(), + "time": DateTime.now().toString(), + "endtime": DateTime.now().add(const Duration(hours: 2)).toString(), + "price": '50', + "passenger_id": box.read(BoxName.passengerID).toString(), + "driver_id": driver['driver_id'].toString(), + "status": "waiting", + 'carType': 'vip', + "price_for_driver": '50', + "price_for_passenger": '50', + "distance": '20', + "paymentMethod": 'cash', + }).then((value) { + if (value is String) { + final parsedValue = jsonDecode(value); + rideId = parsedValue['message']; + } else if (value is Map) { + rideId = value['message']; + } + }); + + driverIdVip = driver['driver_id'].toString(); + driverId = driver['driver_id'].toString(); + + DateTime timeSelected = DateTime.parse(tripDateTime.toIso8601String()); + Get.find().scheduleNotificationsForTimeSelected( + "Your trip is scheduled".tr, + "Don't forget your ride!".tr, + "tone1", + timeSelected); + + await NotificationService.sendNotification( + category: 'OrderVIP', + target: driver['token'].toString(), + title: 'OrderVIP'.tr, + body: '$rideId - VIP Trip', + isTopic: false, + tone: 'tone1', + driverList: [ + id, + rideId, + driver['id'], + passengerLocation.latitude.toString(), + startNameAddress.toString(), + passengerLocation.longitude.toString(), + (box.read(BoxName.name).toString().split(' ')[0]).toString(), + box.read(BoxName.passengerID).toString(), + box.read(BoxName.phone).toString(), + box.read(BoxName.email).toString(), + box.read(BoxName.passengerPhotoUrl).toString(), + box.read(BoxName.tokenFCM).toString(), + (driver['token'].toString()), + ], + ); + if (response['message'] == "Trip updated successfully") { + mySnackbarSuccess("Trip updated successfully".tr); + await NotificationService.sendNotification( + category: 'Order VIP Canceld', + target: response['previous_driver_token'].toString(), + title: 'Order VIP Canceld'.tr, + body: 'Passenger cancel order'.tr, + isTopic: false, + tone: 'cancel', + driverList: [], + ); + } + isBottomSheetShown = false; + update(); + Get.to(() => VipWaittingPage()); + } else { + throw Exception('Failed to save trip'); + } + } catch (e) { + Get.snackbar('Error'.tr, 'Failed to book trip: $e'.tr, + backgroundColor: AppColor.redColor); + } + } + + Future cancelVip(String token, tripId) async { + var res = await CRUD() + .post(link: AppLink.cancelMishwari, payload: {'id': tripId}); + if (res != 'failure') { + Get.back(); + mySnackbarSuccess('You canceled VIP trip'.tr); + } + } + + void sendToDriverAgain(String token) { + NotificationService.sendNotification( + category: 'Order VIP Canceld', + target: token.toString(), + title: 'Order VIP Canceld'.tr, + body: 'Passenger cancel order'.tr, + isTopic: false, + tone: 'cancel', + driverList: [], + ); + } + + Set notifiedDrivers = {}; + + Future processDriverArrival(String source) async { + if (currentRideState.value == RideState.driverArrived || + _isArrivalProcessed) { + Log.print("✋ Ignored Arrival from $source. Already processed."); + return; + } + + _isArrivalProcessed = true; + Log.print("🚖 Driver Arrived via $source! Processing..."); + + currentRideState.value = RideState.driverArrived; + statusRide = 'Arrived'; + await RideLiveNotification.showDriverArrived(driverName); + + uiInteractions.driverArrivePassengerDialoge(); + startTimerDriverWaitPassenger5Minute(); + + if (mapEngine.polylineCoordinates.isNotEmpty) { + mapEngine.playRouteAnimation( + mapEngine.polylineCoordinates, mapEngine.lastComputedBounds); + } + update(); + } + + Future processRideFinished(List driverList, + {String source = "Unknown"}) async { + if (currentRideState.value == RideState.finished || _isFinishProcessed) { + Log.print("✋ Ignored Finish Request from $source. Already Finished."); + return; + } + + _isFinishProcessed = true; + Log.print("🏁 Ride Finished via $source."); + + currentRideState.value = RideState.finished; + mapSocket.disposeRideSocket(); + stopDriverLocationPolling(); + if (Get.isRegistered()) { + Get.find().stopRecording(); + } + + if (Get.isDialogOpen == true) Get.back(); + + NotificationController().showNotification( + 'Alert'.tr, + "Please make sure not to leave any personal belongings in the car.".tr, + 'tone1', + ); + IosLiveActivityService.endRideActivity(); + PipService.disablePip(); + await RideLiveNotification.cancel(); + + if (driverList.length >= 4) { + String price = driverList[3].toString(); + Get.offAll(() => RateDriverFromPassenger(), arguments: { + 'driverId': driverList[0].toString(), + 'rideId': driverList[1].toString(), + 'price': price + }); + } + } + + Future processRideCancelledByDriver(dynamic data, + {String source = "Unknown"}) async { + if (_isCancelProcessed) return; + + _isCancelProcessed = true; + stopAllTimers(); + if (Get.isDialogOpen == true) Get.back(); + await RideLiveNotification.cancel(); + IosLiveActivityService.endRideActivity(); + PipService.disablePip(); + + Get.defaultDialog( + title: "Sorry 😔".tr, + titleStyle: + const TextStyle(color: Colors.red, fontWeight: FontWeight.bold), + barrierDismissible: false, + content: Column( + children: [ + const Icon(Icons.cancel_presentation, + size: 50, color: Colors.redAccent), + const SizedBox(height: 10), + Text( + "The driver cancelled the trip for an emergency reason.\nDo you want to search for another driver immediately?" + .tr, + textAlign: TextAlign.center, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Get.back(); + handleNoDriverFound(); + }, + child: Text("Cancel Trip".tr, + style: const TextStyle(color: Colors.grey)), + ), + ElevatedButton.icon( + style: + ElevatedButton.styleFrom(backgroundColor: AppColor.primaryColor), + icon: const Icon(Icons.refresh, color: Colors.white), + label: Text("Search for another driver".tr, + style: const TextStyle(color: Colors.white)), + onPressed: () { + Get.back(); + retrySearchForDrivers(); + }, + ), + ], + ); + } + + void showNoDriverDialog() { + Get.defaultDialog( + title: "No Drivers Found".tr, + middleText: + "Sorry, there are no cars available of this type right now.".tr, + textConfirm: "Refresh Map".tr, + textCancel: "Cancel".tr, + confirmTextColor: Colors.white, + onConfirm: () { + Get.back(); + restCounter(); + stopAllTimers(); + clearControllersAndGoHome(); + }, + ); + } + + Future + calculateDistanceBetweenPassengerAndDriverBeforeCancelRide() async { + await getDriverCarsLocationToPassengerAfterApplied(); + double dist = Geolocator.distanceBetween( + passengerLocation.latitude, + passengerLocation.longitude, + driverCarsLocationToPassengerAfterApplied.last.latitude, + driverCarsLocationToPassengerAfterApplied.last.longitude, + ); + if (dist > 500) { + isCancelRidePageShown = true; + update(); + } else { + Get.defaultDialog( + barrierDismissible: false, + title: 'The Driver Will be in your location soon .'.tr, + middleText: 'The distance less than 500 meter.'.tr, + confirm: Column( + children: [ + MyElevatedButton( + kolor: AppColor.greenColor, + title: 'Ok'.tr, + onPressed: () { + Get.back(); + }, + ), + MyElevatedButton( + kolor: AppColor.redColor, + title: 'No, I want to cancel this trip'.tr, + onPressed: () { + Get.back(); + MyDialog().getDialog( + 'Attention'.tr, + 'You will be charged for the cost of the driver coming to your location.' + .tr, + () async { + Get.back(); + Get.find() + .payToDriverForCancelAfterAppliedAndHeNearYou(rideId); + }, + ); + }, + ), + ], + ), + ); + } + } + + Future cancelRideAfterRejectFromAll() async { + locSearch.clearPlacesDestination(); + mapEngine.clearPolyline(); + data = []; + await CRUD().post( + link: "${AppLink.server}/ride/rides/cancel_ride_by_passenger.php", + payload: { + "ride_id": rideId.toString(), + "reason": 'notApplyFromAnyDriver' + }); + + rideConfirm = false; + statusRide = 'Cancel'; + isSearchingWindow = false; + shouldFetch = false; + isPassengerChosen = false; + isCashConfirmPageShown = false; + isCashSelectedBeforeConfirmRide = false; + timeToPassengerFromDriverAfterApplied = 0; + mapEngine.changeCancelRidePageShow(); + remainingTime = 0; + update(); + } + + void selectReason0(int index, String note) { + selectedReason = index; + cancelNote = note; + update(); + } + + int selectedReasonIndex = -1; + String selectedReasonText = ""; + TextEditingController otherReasonController = TextEditingController(); + + void selectReason(int index, String reason) { + selectedReasonIndex = index; + selectedReasonText = reason; + update(); + } + + List data = []; + + void restCounter() { + locSearch.clearPlacesDestination(); + mapEngine.clearPolyline(); + data = []; + rideConfirm = false; + shouldFetch = false; + timeToPassengerFromDriverAfterApplied = 0; + update(); + } + + Future _checkAndRefreshMapStyle() async { + try { + final String styleJson = await rootBundle.loadString('assets/style.json'); + final Map decoded = json.decode(styleJson); + final String? currentVersion = + decoded['metadata'] != null ? decoded['metadata']['version'] : null; + + if (currentVersion == null) return; + final String lastVersion = box.read(BoxName.styleVersion) ?? "0.0.0"; + + if (currentVersion != lastVersion) { + Log.print( + "♻️ Map Style Version mismatch ($lastVersion -> $currentVersion). Purging offline cache..."); + await OfflineMapService.instance.clearCache(); + await Future.delayed(const Duration(milliseconds: 500)); + box.write(BoxName.styleVersion, currentVersion); + Log.print("✅ Style Version updated to $currentVersion"); + } + } catch (e) { + Log.print("⚠️ Style version check failed: $e"); + } + } + + void reinit() { + if (currentRideState.value != RideState.noRide && + currentRideState.value != RideState.cancelled) { + Log.print('ℹ️ reinit() skipped: ride is active'); + return; + } + Log.print('🔄 reinit() calling resetAllMapStates and restarting timers...'); + resetAllMapStates(); + stopAllTimers(); + currentRideState.value = RideState.noRide; + + // Restart location search + locSearch.getLocation(); + + // Restart lifecycle timers & stages + getLocationArea(passengerLocation.latitude, passengerLocation.longitude); + unawaited(_stagePricingAndState()); + unawaited(_stageNiceToHave()); + startMasterTimer(); + } + + void resetAllMapStates() { + Log.print('🧹 Resetting all map states to prevent sticky location bug'); + locSearch.clearPlacesDestination(); + locSearch.clearPlacesStart(); + locSearch.waypoints.clear(); + locSearch.clearAllMenuWaypoints(); + if (Get.isRegistered()) { + Get.find().reset(); + } + + // Call reset on mapEngine which handles clearing markers, polylines, animation timers and UI states + mapEngine.reset(); + data = []; + + locSearch.passengerStartLocationFromMap = false; + locSearch.startLocationFromMap = false; + isPickerShown = false; + locSearch.workLocationFromMap = false; + locSearch.homeLocationFromMap = false; + isAnotherOreder = false; + isWhatsAppOrder = false; + + myDestination = passengerLocation; + locSearch.hintTextDestinationPoint = 'Select your destination'.tr; + + locSearch.placeDestinationController.clear(); + locSearch.placeStartController.clear(); + + rideConfirm = false; + shouldFetch = true; // reset to true by default for next ride search polling + isDrawingRoute = false; + isLoading = false; + + // Reset RideLifecycleController specific search and lifecycle states + isSearchingWindow = false; + currentRideState.value = RideState.noRide; + statusRide = 'wait'; + statusRideVip = 'wait'; + statusRideFromStart = false; + isDriverInPassengerWay = false; + isDriverArrivePassenger = false; + _isArrivalProcessed = false; + _isFinishProcessed = false; + _isCancelProcessed = false; + _isAcceptanceProcessed = false; + _isRatingScreenOpen = false; + _isRecalculatingRoute = false; + _isRideStartedProcessed = false; + _isDriverAppliedLogicExecuted = false; + _isDriverArrivedLogicExecuted = false; + _isRideBeginLogicExecuted = false; + _currentDriverRoutePoints = []; + _currentDriverRouteDistanceMeters = 0.0; + _currentDriverRouteDurationSeconds = 0; + _driverEtaUpdatedAt = null; + _driverEtaSecondsAtUpdate = 0; + _driverEtaCountdownTicks = 0; + _routeHeadingMismatchCount = 0; + distanceByPassenger = ''; + durationToPassenger = 0; + stringRemainingTimeToPassenger = ''; + + update(); + } + + void _handleFatalError(String title, String message) { + if (Get.isBottomSheetOpen == true || Get.isDialogOpen == true) { + Get.back(); + } + if (Get.isSnackbarOpen) Get.closeCurrentSnackbar(); + + isDrawingRoute = false; + isLoading = false; + update(); + + Get.defaultDialog( + title: title, + titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), + middleText: message, + middleTextStyle: AppStyle.subtitle, + barrierDismissible: false, + confirm: MyElevatedButton( + title: "Close".tr, + kolor: AppColor.redColor, + onPressed: () { + Get.back(); + clearControllersAndGoHome(); + }, + ), + ); + } + + String shortenAddress(String fullAddress) { + List parts = fullAddress.split('،'); + parts = parts.map((part) => part.trim()).toList(); + parts = parts.where((part) => part.isNotEmpty).toList(); + + String shortAddress = ''; + if (parts.isNotEmpty) { + shortAddress += parts[0]; + } + if (parts.length > 2) { + shortAddress += '، ${parts[2]}'; + } else if (parts.length > 1) { + shortAddress += '، ${parts[1]}'; + } + + if (parts.length > 1) { + shortAddress += '، ${parts.last}'; + } + + shortAddress = shortAddress + .split('،') + .where((part) => !RegExp(r'^[0-9 ]+$').hasMatch(part.trim())) + .join('er'); + + bool isEnglish = + RegExp(r'^[a-zA-Z0-9 ]+$').hasMatch(shortAddress.replaceAll('،', '')); + + if (isEnglish) { + List englishParts = shortAddress.split('،'); + if (englishParts.length > 2) { + shortAddress = + '${englishParts[0]}، ${englishParts[1]}، ${englishParts.last}'; + } else if (englishParts.length > 1) { + shortAddress = '${englishParts[0]}، ${englishParts.last}'; + } + } + return shortAddress; + } + + double distanceOfDestination = 0; + bool haveSteps = false; + + Future getMapPoints( + String originSteps, String destinationSteps, int index) async { + isWayPointStopsSheetUtilGetMap = false; + await nearbyDrivers.getCarsLocationByPassengerAndReloadMarker(); + update(); + + var url = + ('${AppLink.googleMapsLink}directions/json?&language=${box.read(BoxName.lang)}&avoid=tolls|ferries&destination=$destinationSteps&origin=$originSteps&key=${AK.mapAPIKEY}'); + var response = await CRUD().getGoogleApi(link: url, payload: {}); + + data = response['routes'][0]['legs']; + + int durationToRide0 = data[0]['duration']['value']; + durationToRide = durationToRide + durationToRide0; + distance = distanceOfDestination + (data[0]['distance']['value']) / 1000; + + update(); + final String pointsString = + response['routes'][0]["overview_polyline"]["points"]; + + List decodedPoints = + await compute(decodePolylineIsolate, pointsString); + for (int i = 0; i < decodedPoints.length; i++) { + mapEngine.polylineCoordinates.add(decodedPoints[i]); + } + + if (polyLines.isEmpty) { + var polyline = Polyline( + polylineId: PolylineId('route_$index'), + points: locSearch.polylineCoordinatesPointsAll[index], + width: 6, + color: const Color(0xFF2196F3), + ); + + polyLines = {...polyLines, polyline}; + rideConfirm = false; + update(); + } + } + + void updateCameraForDistanceAfterGetMap() { + LatLng coord1 = LatLng( + double.parse(locSearch.coordinatesWithoutEmpty.first.split(',')[0]), + double.parse(locSearch.coordinatesWithoutEmpty.first.split(',')[1])); + + LatLng coord2 = LatLng( + double.parse(locSearch.coordinatesWithoutEmpty.last.split(',')[0]), + double.parse(locSearch.coordinatesWithoutEmpty.last.split(',')[1])); + + LatLng northeastBound; + LatLng southwestBound; + + if (coord1.latitude > coord2.latitude) { + northeastBound = coord1; + southwestBound = coord2; + } else { + northeastBound = coord2; + southwestBound = coord1; + } + + LatLngBounds boundsObj = + LatLngBounds(northeast: northeastBound, southwest: southwestBound); + var cameraUpdate = CameraUpdate.newLatLngBounds(boundsObj, + left: 180, top: 180, right: 180, bottom: 180); + mapController!.animateCamera(cameraUpdate); + update(); + } + + int selectedIndex = -1; + void selectCarFromList(int index) { + selectedIndex = index; + carTypes.forEach((element) => element.isSelected = false); + carTypes[index].isSelected = true; + update(); + } + + Future showBottomSheet1() async { + await bottomSheet(); + isBottomSheetShown = true; + heightBottomSheetShown = 250; + update(); + } + + double calculateAngleBetweenLocations(LatLng start, LatLng end) { + double startLat = start.latitude * pi / 180; + double startLon = start.longitude * pi / 180; + double endLat = end.latitude * pi / 180; + double endLon = end.longitude * pi / 180; + + double dLon = endLon - startLon; + + double y = sin(dLon) * cos(endLat); + double x = + cos(startLat) * sin(endLat) - sin(startLat) * cos(endLat) * cos(dLon); + + double angle = atan2(y, x); + double angleDegrees = angle * 180 / pi; + + return angleDegrees; + } + + get dataCarsLocationByPassenger { + return nearbyDrivers.carsLocationByPassenger; + } + + set dataCarsLocationByPassenger(var val) { + nearbyDrivers.carsLocationByPassenger = val; + } + + double calculateBearing(double lat1, double lon1, double lat2, double lon2) { + return nearbyDrivers.calculateBearing(lat1, lon1, lat2, lon2); + } + + void analyzeBehavior(Position currentPosition, List routePoints) { + nearbyDrivers.analyzeBehavior(currentPosition, routePoints); + } + + void detectStops(Position currentPosition) { + nearbyDrivers.detectStops(currentPosition); + } + + Future getDirectionMap(String origin, String destination, + [List waypoints = const [], int attemptCount = 0]) async { + if (attemptCount == 0) { + isDrawingRoute = true; + update(); + if (isDrawingRoute) showDrawingBottomSheet(); + + await nearbyDrivers.getCarsLocationByPassengerAndReloadMarker(); + } + + if (origin.isEmpty) { + origin = '${passengerLocation.latitude},${passengerLocation.longitude}'; + } + + var coordDestination = destination.split(','); + double latDest = double.parse(coordDestination[0]); + double lngDest = double.parse(coordDestination[1]); + myDestination = LatLng(latDest, lngDest); + + Uri uri; + + var originCoords = origin.split(','); + final Map queryParams = { + 'fromLat': originCoords[0].trim(), + 'fromLng': originCoords[1].trim(), + 'toLat': latDest.toString(), + 'toLng': lngDest.toString(), + }; + + for (int i = 0; i < activeMenuWaypointCount; i++) { + final wp = menuWaypoints[i]; + if (wp != null) { + queryParams['stop${i + 1}Lat'] = wp.latitude.toString(); + queryParams['stop${i + 1}Lng'] = wp.longitude.toString(); + } + } + + uri = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams); + + Log.print( + 'Requesting Route URI (SaaS, Attempt: ${attemptCount + 1}): $uri'); + + http.Response response; + Map responseData; + + try { + response = await http.get(uri, headers: { + 'x-api-key': Env.mapSaasKey, + }).timeout(const Duration(seconds: 20)); + + responseData = json.decode(response.body); + + bool isRequestValid = response.statusCode == 200; + + if (!isRequestValid) { + if (attemptCount < 2) { + await _retryProcess(origin, destination, waypoints, attemptCount); + return; + } + _handleFatalError( + "Server Error".tr, "Connection failed. Please try again.".tr); + return; + } + + double apiDistanceMeters; + String pointsString; + dynamic routeData; + + apiDistanceMeters = (responseData['distance'] as num).toDouble(); + pointsString = responseData['points'] ?? ""; + routeData = responseData; + + var origCoords = origin.split(','); + double startLat = double.parse(origCoords[0]); + double startLng = double.parse(origCoords[1]); + + double aerialDistance = + Geolocator.distanceBetween(startLat, startLng, latDest, lngDest); + + if (apiDistanceMeters < 50.0 && aerialDistance > 200.0) { + Log.print( + "⚠️ Suspicious Route detected! Server: $apiDistanceMeters m | Aerial: $aerialDistance m"); + + if (attemptCount < 2) { + Log.print("🔄 Retrying request (Attempt ${attemptCount + 2})..."); + await Future.delayed(const Duration(seconds: 1)); + await getDirectionMap( + origin, destination, waypoints, attemptCount + 1); + return; + } else { + Log.print("❌ All retries failed. Calculating Route is impossible."); + _handleFatalError( + "Route Not Found".tr, + "We couldn't find a valid route to this destination. Please try selecting a different point." + .tr); + return; + } + } + + box.remove(BoxName.tripData); + box.write(BoxName.tripData, routeData); + + durationToRide = + ((routeData['duration'] as num) * kDurationScalar).toInt(); + double distanceOfTrip = apiDistanceMeters / 1000.0; + distance = distanceOfTrip; + + data = routeData['legs'] != null && routeData['legs'].isNotEmpty + ? (routeData['legs'][0]['steps'] ?? []) + : []; + + List decodedPoints = []; + if (pointsString.isNotEmpty) { + decodedPoints = await compute(decodePolylineIsolate, pointsString); + } + + if (decodedPoints.isEmpty) { + _handleFatalError("Map Error".tr, "Received empty route data.".tr); + return; + } + + mapEngine.polylineCoordinates.clear(); + mapEngine.polylineCoordinates.addAll(decodedPoints); + + final LatLng startLoc = mapEngine.polylineCoordinates.first; + final LatLng endLoc = mapEngine.polylineCoordinates.last; + + startNameAddress = responseData['startName'] ?? 'Start Point'.tr; + endNameAddress = responseData['endName'] ?? 'Destination'.tr; + Log.print('📍 ROUTE START: $startNameAddress'); + Log.print('📍 ROUTE END: $endNameAddress'); + + if (responseData['bbox'] != null) { + List bbox = responseData['bbox']; + if (bbox.length == 4) { + mapEngine.lastComputedBounds = LatLngBounds( + southwest: LatLng(bbox[1], bbox[0]), + northeast: LatLng(bbox[3], bbox[2]), + ); + } + } else { + double? minLat, maxLat, minLng, maxLng; + for (LatLng point in mapEngine.polylineCoordinates) { + minLat = + minLat == null ? point.latitude : min(minLat, point.latitude); + maxLat = + maxLat == null ? point.latitude : max(maxLat, point.latitude); + minLng = + minLng == null ? point.longitude : min(minLng, point.longitude); + maxLng = + maxLng == null ? point.longitude : max(maxLng, point.longitude); + } + if (minLat != null) { + mapEngine.lastComputedBounds = LatLngBounds( + northeast: LatLng(maxLat!, maxLng!), + southwest: LatLng(minLat!, minLng!)); + } + } + + if (isDrawingRoute) { + Log.print('🔔 Finalizing route drawing state'); + isDrawingRoute = false; + isLoading = false; + update(); + } + + durationToAdd = Duration(seconds: durationToRide); + hours = durationToAdd.inHours; + minutes = (durationToAdd.inMinutes % 60).round(); + + markers = { + Marker( + markerId: const MarkerId('start'), + position: startLoc, + icon: InlqBitmap.fromStyleImage('orange_marker'), + infoWindow: const InfoWindow(title: 'A'), + anchor: const Offset(0.5, 1.0), + ), + Marker( + markerId: const MarkerId('end'), + position: endLoc, + icon: InlqBitmap.fromStyleImage('violet_marker'), + infoWindow: const InfoWindow(title: 'B'), + anchor: const Offset(0.5, 1.0), + ), + }; + + for (int i = 0; i < activeMenuWaypointCount; i++) { + final wp = menuWaypoints[i]; + if (wp != null) { + final bool isFirstWaypoint = i == 0; + markers.add(Marker( + markerId: MarkerId('waypoint_$i'), + position: wp, + icon: InlqBitmap.fromStyleImage( + isFirstWaypoint ? 'orange_marker' : 'violet_marker'), + infoWindow: + InfoWindow(title: isFirstWaypoint ? 'Stop 1' : 'Stop 2'), + anchor: const Offset(0.5, 1.0), + )); + } + } + + if (polyLines.isNotEmpty) mapEngine.clearPolyline(); + + rideConfirm = false; + isMarkersShown = true; + update(); + + await bottomSheet(); + + await mapEngine.playRouteAnimation( + mapEngine.polylineCoordinates, mapEngine.lastComputedBounds); + } catch (e, stackTrace) { + if (isDrawingRoute) { + isDrawingRoute = false; + isLoading = false; + update(); + } + + Log.print('🚨 CRITICAL ERROR IN getDirectionMap: $e'); + Log.print('🚨 STACKTRACE: $stackTrace'); + + if (attemptCount < 2) { + await _retryProcess(origin, destination, waypoints, attemptCount); + } else { + _handleFatalError("Connection Error".tr, + "Please check your internet and try again.".tr); + } + } + } + + Future _retryProcess(String origin, String dest, List waypoints, + int currentAttempt) async { + Log.print( + "🔄 Exception or Error caught. Retrying in 1s... (Attempt ${currentAttempt + 1})"); + await Future.delayed(const Duration(seconds: 1)); + getDirectionMap(origin, dest, waypoints, currentAttempt + 1); + } + + bool _isUsingFallback = false; + + void _startPollingFallback() { + if (_isUsingFallback) return; + + Log.print('🔄 Starting Polling Fallback Mode'); + _isUsingFallback = true; + + startMasterTimer(); + } + + Future _restorePolyline(String polylineString) async { + try { + List points = + await compute(decodePolylineIsolate, polylineString); + + mapEngine.polylineCoordinates.clear(); + mapEngine.polylineCoordinates.addAll(points); + + mapEngine.clearPolyline(); + mapEngine.polyLines = { + ...mapEngine.polyLines, + Polyline( + polylineId: const PolylineId('route_direct'), + points: mapEngine.polylineCoordinates, + color: const Color(0xFF2196F3), + width: 6, + ) + }; + + update(); + } catch (e) { + Log.print('Error restoring polyline: $e'); + } + } + + Future processRideAcceptance( + {Map? driverData, required String source}) async { + if (_isAcceptanceProcessed || + currentRideState.value == RideState.driverApplied || + currentRideState.value == RideState.driverArrived || + currentRideState.value == RideState.inProgress) { + Log.print("✋ Ignored Acceptance from $source. Already processed."); + return; + } + + _rideAcceptedViaSource = source; + + _isAcceptanceProcessed = true; + _isDriverAppliedLogicExecuted = true; + Log.print("🚀 Winner: $source triggered acceptance! Processing..."); + + _masterTimer?.cancel(); + + currentRideState.value = RideState.driverApplied; + statusRide = 'Apply'; + isSearchingWindow = false; + + if (driverData != null && driverData.isNotEmpty) { + Log.print("📥 Populating Data from $source payload..."); + _fillDriverDataLocally(driverData); + } else { + Log.print("⚠️ No Data in Payload. Fallback to API."); + await getUpdatedRideForDriverApply(rideId); + } + + await IosLiveActivityService.startRideActivity( + rideId: rideId, + driverName: driverName, + carDetails: '$make • $carColor', + etaText: stringRemainingTimeToPassenger, + progress: 0.0, + ); + + _showRideStartNotifications(); + final etaText = stringRemainingTimeToPassenger; + final carInfo = '$make • $model • $licensePlate'; + + await RideLiveNotification.showDriverOnWay( + driverName: driverName, + etaText: etaText, + carInfo: carInfo, + ); + + update(); + + await getDriverCarsLocationToPassengerAfterApplied(); + _startSocketWatchdog(); + + if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) { + LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last; + + await calculateDriverToPassengerRoute(driverPos, passengerLocation); + + startTimerFromDriverToPassengerAfterApplied(); + } + + PipService.enablePip(); + + if (source == "Socket" && mapSocket.isSocketConnected) { + Log.print( + "🧠 Smart Mode: Socket accepted ride. Skipping polling, relying on WebSocket."); + } else { + Log.print("🔄 Fallback Mode: $source accepted ride. Starting polling."); + _startDriverLocationPollingWithTimer(); + } + } + + void _fillDriverDataLocally(Map data) { + try { + driverId = data['driver_id']?.toString() ?? ''; + driverPhone = data['phone']?.toString() ?? ''; + + String fName = (data['first_name'] ?? data['driver_first_name'] ?? '') + .toString() + .trim(); + String lName = (data['last_name'] ?? data['driver_last_name'] ?? '') + .toString() + .trim(); + final socketDriverName = + (data['driverName'] ?? data['driver_name'] ?? '').toString().trim(); + driverName = socketDriverName.isNotEmpty + ? socketDriverName + : [fName, lName].where((part) => part.isNotEmpty).join(' '); + + make = data['make']?.toString() ?? ''; + model = data['model']?.toString() ?? ''; + carColor = data['color']?.toString() ?? ''; + colorHex = data['color_hex']?.toString() ?? ''; + licensePlate = data['car_plate']?.toString() ?? ''; + carYear = data['year']?.toString() ?? ''; + + driverRate = data['ratingDriver']?.toString() ?? '5.0'; + driverToken = data['token']?.toString() ?? ''; + + update(); + } catch (e) { + Log.print("Error parsing socket driver data: $e"); + } + } + + Future cancelRide() async { + if (selectedReasonIndex == -1) { + Get.snackbar( + 'Attention'.tr, + 'Please select a reason first'.tr, + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.orange, + colorText: Colors.white, + ); + return; + } + + String finalReason = selectedReasonText; + if (finalReason == "Other".tr) { + if (otherReasonController.text.trim().isEmpty) { + Get.snackbar("Attention".tr, "Please write the reason...".tr, + backgroundColor: Colors.red, colorText: Colors.white); + return; + } + finalReason = otherReasonController.text.trim(); + } + + Get.back(); + if (isCancelRidePageShown) { + mapEngine.changeCancelRidePageShow(); + } + + resetAllMapStates(); + + stopAllTimers(); + currentRideState.value = RideState.cancelled; + await RideLiveNotification.cancel(); + IosLiveActivityService.endRideActivity(); + PipService.disablePip(); + + if (rideId != 'yet' && rideId != null) { + Log.print( + '📡 Sending Cancel Request to Server with Reason: $finalReason'); + + try { + await CRUD().post( + link: "${AppLink.server}/ride/rides/cancel_ride_by_passenger.php", + payload: { + "ride_id": rideId.toString(), + "reason": finalReason, + "driver_token": driverToken, + }, + ); + } catch (e) { + Log.print("Error cancelling on server: $e"); + } + } + + clearControllersAndGoHome(); + } + + Future getAIKey(String key) async { + var res = + await CRUD().get(link: AppLink.getapiKey, payload: {"keyName": key}); + if (res != 'failure') { + var d = jsonDecode(res)['message']; + return d[key].toString(); + } + return null; + } + + Future getRideStatus(String rideId) async { + final response = await CRUD().get( + link: "${AppLink.rideServerSide}/ride/rides/getRideStatus.php", + payload: {'id': rideId}); + Log.print(response); + Log.print('2176'); + return jsonDecode(response)['data']; + } + + void handleActiveRideOnStartup(dynamic data) { + try { + if (data == null || data['has_active_ride'] != true) { + Log.print('[Startup] No active ride'); + currentRideState.value = RideState.noRide; + startMasterTimer(); + return; + } + + Log.print('[Startup] ✅ Active ride found!'); + + var rideData = data['ride']; + rideId = rideData['ride_id'].toString(); + currentRideId = rideId; + driverId = rideData['driver_id']?.toString() ?? ''; + + String status = rideData['status']?.toString().toLowerCase() ?? ''; + + if (status == 'waiting' || status == 'searching') { + currentRideState.value = RideState.searching; + isSearchingWindow = true; + } else if (status == 'apply' || status == 'applied') { + currentRideState.value = RideState.driverApplied; + statusRide = 'Apply'; + + mapSocket.socket.emit('subscribe_driver_location', { + 'ride_id': rideId, + 'driver_id': driverId, + }); + + if (rideData['driver_info'] != null) { + var dInfo = rideData['driver_info']; + passengerName = dInfo['first_name']?.toString() ?? ''; + driverPhone = dInfo['phone']?.toString() ?? ''; + model = dInfo['model']?.toString() ?? ''; + licensePlate = dInfo['license_plate']?.toString() ?? ''; + } + } else if (status == 'arrived') { + currentRideState.value = RideState.driverArrived; + statusRide = 'Arrived'; + isDriverArrivePassenger = true; + } else if (status == 'begin' || status == 'started') { + currentRideState.value = RideState.inProgress; + statusRide = 'Begin'; + rideTimerBegin = true; + + if (rideData['polyline'] != null) { + _restorePolyline(rideData['polyline']); + } + + rideIsBeginPassengerTimer(); + } + + update(); + startMasterTimer(); + } catch (e) { + Log.print('[Startup] Error: $e'); + currentRideState.value = RideState.noRide; + startMasterTimer(); + } + } + + Future handleNoDriverFound() async { + stopAllTimers(); + await RideLiveNotification.cancel(); + IosLiveActivityService.endRideActivity(); + PipService.disablePip(); + _isCancelProcessed = false; + currentRideState.value = RideState.noRide; + resetAllMapStates(); + clearControllersAndGoHome(); + + Get.defaultDialog( + title: "We apologize 😔".tr, + middleText: "No drivers found at the moment.\nPlease try again later.".tr, + confirm: ElevatedButton( + onPressed: () => Navigator.pop(Get.context!), + child: Text("Ok".tr), + ), + ); + } + + bool isDriversDataValid() { + return dataCarsLocationByPassenger != 'failure' && + dataCarsLocationByPassenger != null && + (dataCarsLocationByPassenger is Map) && + dataCarsLocationByPassenger.containsKey('message') && + dataCarsLocationByPassenger['message'] != null; + } + + void retrySearchForDrivers() async { + _isCancelProcessed = false; + isSearchingWindow = true; + currentRideState.value = RideState.searching; + driversStatusForSearchWindow = 'Searching for nearby drivers...'.tr; + update(); + + try { + Log.print("🔄 Retrying search for ride ID: $rideId"); + + var payload = { + "ride_id": rideId.toString(), + "passenger_id": box.read(BoxName.passengerID).toString(), + "passenger_name": box.read(BoxName.name).toString(), + "passenger_phone": box.read(BoxName.phone).toString(), + "passenger_email": box.read(BoxName.email).toString(), + "passenger_token": box.read(BoxName.tokenFCM).toString(), + "passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(), + "passenger_rating": "5.0", + "start_lat": startLocation.latitude.toString(), + "start_lng": startLocation.longitude.toString(), + "end_lat": endLocation.latitude.toString(), + "end_lng": endLocation.longitude.toString(), + "start_name": startNameAddress, + "end_name": endNameAddress, + "distance": distance.toString(), + "distance_text": distanceByPassenger, + "duration_text": durationToPassenger.toString(), + "price": totalPassenger.toString(), + "price_for_driver": costForDriver.toString(), + "car_type": box.read(BoxName.carType).toString(), + "is_wallet": Get.find().isWalletChecked.toString(), + "has_steps": Get.find().wayPoints.length > 1 + ? "true" + : "false", + }; + + var response = await CRUD().post( + link: "${AppLink.rideServerSide}/rides/retry_search_drivers.php", + payload: payload, + ); + + if (response['status'] == 'success') { + Log.print("✅ Search reset successfully."); + startSearchingTimer(); + } else { + Log.print("❌ Failed to reset search: $response"); + handleNoDriverFound(); + } + } catch (e) { + Log.print("❌ Exception in retrySearchForDrivers: $e"); + handleNoDriverFound(); + } + } + + Future startSearchingTimer() async { + _searchTimer?.cancel(); + int seconds = 0; + + Log.print("⏳ Search Timer Started (90s)..."); + await RideLiveNotification.showSearching(driversStatusForSearchWindow); + + _searchTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + seconds++; + + if (currentRideState.value != RideState.searching) { + timer.cancel(); + return; + } + + if (seconds >= 90) { + timer.cancel(); + handleNoDriverFound(); + } + }); + } + + void showNoDriversDialog() { + Get.dialog( + BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: CupertinoAlertDialog( + title: Text("No Car or Driver Found in your area.".tr, + style: AppStyle.title + .copyWith(fontSize: 20, fontWeight: FontWeight.bold)), + content: Text("No Car or Driver Found in your area.".tr, + style: AppStyle.title.copyWith(fontSize: 16)), + actions: [ + CupertinoDialogAction( + onPressed: () { + Get.back(); + clearControllersAndGoHome(); + }, + child: + Text('OK'.tr, style: TextStyle(color: AppColor.greenColor)), + ), + ], + ), + ), + barrierDismissible: false, + ); + } + + Future getDistanceFromDriverAfterAcceptedRide( + String origin, String destination) async { + String apiKey = Env.mapKeyOsm; + if (origin.isEmpty) { + origin = '${passengerLocation.latitude},${passengerLocation.longitude}'; + } + var uri = Uri.parse( + '$dynamicApiUrl?origin=$origin&destination=$destination&steps=false&overview=false'); + Log.print('uri: $uri'); + + http.Response response; + Map responseData; + + try { + response = await http.get( + uri, + headers: { + 'X-API-KEY': apiKey, + }, + ).timeout(const Duration(seconds: 20)); + + if (response.statusCode != 200) { + Log.print('Error from API: ${response.statusCode}'); + isLoading = false; + update(); + return; + } + if (Get.isBottomSheetOpen ?? false) { + Get.back(); + } + isDrawingRoute = false; + + responseData = json.decode(response.body); + Log.print('responseData: $responseData'); + + if (responseData['status'] != 'ok') { + Log.print('API returned an error: ${responseData['message']}'); + isLoading = false; + update(); + return; + } + } catch (e) { + Log.print('Failed to get directions: $e'); + isLoading = false; + update(); + return; + } + } + + Future _stageNiceToHave() async { + Log.print('🚀 MapPassengerController: Starting _stageNiceToHave'); + + await Future.wait([ + Future(() async { + try { + Log.print('🔍 Loading Favorites...'); + await locSearch.getFavioratePlaces(); + } catch (e) { + Log.print("Error: $e"); + } + }), + Future(() async { + try { + Log.print('🔍 Loading Waypoints...'); + locSearch.readyWayPoints(); + } catch (e) { + Log.print("Error: $e"); + } + }), + Future(() async { + try { + Log.print('🔍 Loading Rate...'); + await getPassengerRate(); + } catch (e) { + Log.print("Error: $e"); + } + }), + Future(() async { + try { + Log.print('🔍 Loading Coupons...'); + await firstTimeRunToGetCoupon(); + } catch (e) { + Log.print("Error: $e"); + } + }), + ]); + Log.print('✅ MapPassengerController: _stageNiceToHave complete'); + try { + cardNumber = await SecureStorage().readData(BoxName.cardNumber); + } catch (e) { + Log.print("Error: $e"); + } + } + + Future _stagePricingAndState() async { + try { + await getKazanPercent(); + } catch (e) { + Log.print("Error: $e"); + } + try { + await _checkInitialRideStatus(); + } catch (e) { + Log.print("Error: $e"); + } + _applyLowEndModeIfNeeded(); + } + + void _applyLowEndModeIfNeeded() { + // Placeholder comment from original + } + + void showDrawingBottomSheet() { + Log.print( + '🔔 showDrawingBottomSheet called. isDrawingRoute: $isDrawingRoute'); + + final context = Get.context; + if (context == null) return; + + WidgetsBinding.instance.addPostFrameCallback((_) { + // Close any existing open dialogs first + if (Get.isDialogOpen == true) { + Get.back(); + } + + Get.dialog( + Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + padding: const EdgeInsets.all(24), + width: 180, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.95), + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 20, + spreadRadius: 5, + ) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // App Logo + Image.asset( + 'assets/images/logo.gif', + height: 64, + errorBuilder: (context, error, stackTrace) => const Icon( + Icons.map, + size: 64, + color: AppColor.primaryColor, + ), + ), + const SizedBox(height: 16), + const SizedBox( + width: 24, + height: 24, + child: MyCircularProgressIndicator(), + ), + const SizedBox(height: 16), + Text( + 'Drawing route on map...'.tr, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: AppColor.primaryColor, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + barrierDismissible: false, + ); + + // Auto-dismiss after exactly 2 seconds + Future.delayed(const Duration(seconds: 2), () { + if (Get.isDialogOpen == true) { + Get.back(); + } + }); + }); + } + + @override + void onInit() async { + super.onInit(); + await _checkAndRefreshMapStyle(); + Get.put(DeepLinkController(), permanent: true); + await initilizeGetStorage(); + getLocationArea(passengerLocation.latitude, passengerLocation.longitude); + unawaited(_stagePricingAndState()); + unawaited(_stageNiceToHave()); + startMasterTimer(); + } + + @override + void onClose() { + stopAllTimers(); + if (!_timerStreamController.isClosed) { + _timerStreamController.close(); + } + if (!_beginRideStreamController.isClosed) { + _beginRideStreamController.close(); + } + if (!_rideStatusStreamController.isClosed) { + _rideStatusStreamController.close(); + } + if (!timerController.isClosed) { + timerController.close(); + } + super.onClose(); + } + + void clearControllersAndGoHome() { + Get.offAll(() => const MapPagePassenger()); + } + + /// Builds a Set of short [Polyline] segments that simulate a dashed line. + /// intaleq_maps (MapLibre) doesn't support `patterns`, so we manually + /// split the route into dash/gap alternating segments. + Set _buildDashedPolylines({ + required List points, + required double dashLengthMeters, + required double gapLengthMeters, + required Color color, + required int width, + required String idPrefix, + }) { + final Set result = {}; + if (points.length < 2) return result; + + int segmentIndex = 0; + bool isDash = true; + double remaining = dashLengthMeters; + List currentSegment = [points[0]]; + + for (int i = 0; i < points.length - 1; i++) { + final LatLng a = points[i]; + final LatLng b = points[i + 1]; + double segLen = _haversineDistance(a, b); + double covered = 0.0; + + while (covered < segLen) { + double leftInSeg = segLen - covered; + if (remaining <= leftInSeg) { + // interpolate the endpoint of this dash/gap + double fraction = (covered + remaining) / segLen; + LatLng interp = LatLng( + a.latitude + fraction * (b.latitude - a.latitude), + a.longitude + fraction * (b.longitude - a.longitude), + ); + currentSegment.add(interp); + + if (isDash && currentSegment.length >= 2) { + result.add(Polyline( + polylineId: PolylineId('${idPrefix}_seg_$segmentIndex'), + points: List.from(currentSegment), + color: color, + width: width, + )); + } + + final double consumed = remaining; + segmentIndex++; + isDash = !isDash; + remaining = isDash ? dashLengthMeters : gapLengthMeters; + currentSegment = [interp]; + covered += consumed; + } else { + currentSegment.add(b); + covered = segLen; + remaining -= leftInSeg; + } + } + } + + // Flush last dash segment + if (isDash && currentSegment.length >= 2) { + result.add(Polyline( + polylineId: PolylineId('${idPrefix}_seg_$segmentIndex'), + points: List.from(currentSegment), + color: color, + width: width, + )); + } + + return result; + } + + /// Haversine distance in meters between two LatLng points. + LatLng? _parseLatLng(String? raw) { + if (raw == null || raw.trim().isEmpty) return null; + final parts = raw.split(','); + if (parts.length < 2) return null; + final lat = double.tryParse(parts[0].trim()); + final lng = double.tryParse(parts[1].trim()); + if (lat == null || lng == null) return null; + if (lat == 0 && lng == 0) return null; + return LatLng(lat, lng); + } + + bool _isHeadingAwayFromRoute({ + required double? heading, + required double? speed, + required int closestRouteIndex, + required double distanceFromRouteMeters, + }) { + if (heading == null || speed == null || speed < 2.5) return false; + if (_currentDriverRoutePoints.length < 2) return false; + if (distanceFromRouteMeters < 10) return false; + + int fromIndex = closestRouteIndex; + int toIndex = + min(closestRouteIndex + 1, _currentDriverRoutePoints.length - 1); + if (fromIndex == toIndex && fromIndex > 0) { + fromIndex--; + } + if (fromIndex == toIndex) return false; + + final double routeBearing = _bearingBetween( + _currentDriverRoutePoints[fromIndex], + _currentDriverRoutePoints[toIndex], + ); + final double angleDiff = _angleDifference(heading, routeBearing); + return angleDiff > 110; + } + + double _bearingBetween(LatLng a, LatLng b) { + final double lat1 = a.latitude * pi / 180; + final double lat2 = b.latitude * pi / 180; + final double dLng = (b.longitude - a.longitude) * pi / 180; + final double y = sin(dLng) * cos(lat2); + final double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLng); + return (atan2(y, x) * 180 / pi + 360) % 360; + } + + double _angleDifference(double a, double b) { + final double diff = ((a - b + 540) % 360) - 180; + return diff.abs(); + } + + double _pathDistanceMeters(List points) { + if (points.length < 2) return 0.0; + double total = 0.0; + for (int i = 0; i < points.length - 1; i++) { + total += _haversineDistance(points[i], points[i + 1]); + } + return total; + } + + double _haversineDistance(LatLng a, LatLng b) { + const R = 6371000.0; + final dLat = (b.latitude - a.latitude) * pi / 180; + final dLng = (b.longitude - a.longitude) * pi / 180; + final sinLat = sin(dLat / 2); + final sinLng = sin(dLng / 2); + final h = sinLat * sinLat + + cos(a.latitude * pi / 180) * + cos(b.latitude * pi / 180) * + sinLng * + sinLng; + return 2 * R * atan2(pow(h, 0.5).toDouble(), pow(1 - h, 0.5).toDouble()); + } +} diff --git a/lib/controller/home/map/ride_state.dart b/lib/controller/home/map/ride_state.dart new file mode 100644 index 0000000..83ce70c --- /dev/null +++ b/lib/controller/home/map/ride_state.dart @@ -0,0 +1,10 @@ +enum RideState { + noRide, // لا يوجد رحلة جارية، عرض واجهة البحث + cancelled, // تم إلغاء الرحلة + preCheckReview, // يوجد رحلة منتهية، تحقق من التقييم + searching, // جاري البحث عن كابتن + driverApplied, // تم قبول الطلب + driverArrived, // وصل السائق + inProgress, // الرحلة بدأت بالفعل + finished, // انتهت الرحلة (سيتم تحويلها إلى preCheckReview) +} diff --git a/lib/controller/home/map/ui_interactions_controller.dart b/lib/controller/home/map/ui_interactions_controller.dart new file mode 100644 index 0000000..388c28e --- /dev/null +++ b/lib/controller/home/map/ui_interactions_controller.dart @@ -0,0 +1,436 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:get/get.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; + +import '../../../constant/box_name.dart'; +import '../../../constant/colors.dart'; +import '../../../constant/links.dart'; +import '../../../constant/style.dart'; +import '../../../constant/info.dart'; +import '../../../main.dart'; // contains global 'box' +import '../../../print.dart'; +import '../../../services/emergency_signal_service.dart'; +import '../../../views/widgets/elevated_btn.dart'; +import '../../../views/widgets/mydialoug.dart'; +import '../../../views/widgets/my_textField.dart'; +import '../../../views/home/map_page_passenger.dart'; +import '../../../views/widgets/error_snakbar.dart'; +import '../../../models/model/painter_copoun.dart'; +import '../../functions/launch.dart'; +import '../../firebase/local_notification.dart'; +import '../../firebase/notification_service.dart'; +import '../../functions/crud.dart'; +import '../../functions/tts.dart'; +import 'ride_lifecycle_controller.dart'; +import 'location_search_controller.dart'; +import 'map_engine_controller.dart'; + +class UiInteractionsController extends GetxController { + TextEditingController sosPhonePassengerProfile = TextEditingController(); + TextEditingController whatsAppLocationText = TextEditingController(); + final sosFormKey = GlobalKey(); + + @override + void onInit() { + super.onInit(); + EmergencySignalService.instance.startListening(() { + final rideLifecycle = Get.find(); + if (rideLifecycle.statusRide == 'Begin' || + rideLifecycle.statusRide == 'start') { + Log.print("🚨 Emergency shake verified! Prompting SOS..."); + sosPassenger(); + } + }); + } + + Future _ensureSosNumber(Function onSuccess) async { + String? storedPhone = box.read(BoxName.sosPhonePassenger); + if (storedPhone != null && storedPhone.isNotEmpty) { + onSuccess(); + return; + } + + sosPhonePassengerProfile.clear(); + Get.defaultDialog( + title: 'Add SOS Phone'.tr, + titleStyle: AppStyle.title, + content: Form( + key: sosFormKey, + child: Column( + children: [ + MyTextForm( + controller: sosPhonePassengerProfile, + label: 'insert sos phone'.tr, + hint: 'e.g. 0912345678 (Default +963)'.tr, + type: TextInputType.phone, + ), + const SizedBox(height: 10), + Text( + "Note: If no country code is entered, it will be saved as Syrian (+963).".tr, + style: TextStyle(fontSize: 12, color: Colors.grey), + textAlign: TextAlign.center, + ), + ], + ), + ), + confirm: MyElevatedButton( + title: 'Save'.tr, + onPressed: () async { + if (sosFormKey.currentState!.validate()) { + Get.back(); + var numberPhone = + formatSyrianPhoneNumber(sosPhonePassengerProfile.text); + + await CRUD().post( + link: AppLink.updateprofile, + payload: { + 'id': box.read(BoxName.passengerID), + 'sosPhone': numberPhone, + }, + ); + + box.write(BoxName.sosPhonePassenger, numberPhone); + onSuccess(); + } + }, + ), + cancel: MyElevatedButton( + title: 'Cancel'.tr, + onPressed: () => Get.back(), + kolor: AppColor.redColor, + ) + ); + } + + void sosPassenger() { + _ensureSosNumber(() { + Get.defaultDialog( + barrierDismissible: false, + title: "Emergency SOS".tr, + titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), + content: Column( + children: [ + Icon(Icons.warning_amber_rounded, size: 50, color: AppColor.redColor), + const SizedBox(height: 10), + Text( + "Do you want to send an emergency message to your SOS contact?".tr, + textAlign: TextAlign.center, + style: AppStyle.title, + ), + ], + ), + confirm: MyElevatedButton( + title: "Send SOS".tr, + kolor: AppColor.redColor, + onPressed: () { + Get.back(); + _shareTripDetailsSOS(); + }, + ), + cancel: MyElevatedButton( + title: "I'm Safe".tr, + kolor: AppColor.greenColor, + onPressed: () { + Get.back(); + }, + ), + ); + }); + } + + void _shareTripDetailsSOS() { + final rideLifecycle = Get.find(); + final locSearch = Get.find(); + + String message = "**Emergency SOS from Passenger:**\n"; + String origin = locSearch.startNameAddress; + String destination = locSearch.endNameAddress; + + message += "* ${'Origin'.tr}: $origin\n"; + message += "* ${'Destination'.tr}: $destination\n"; + message += "* ${'Driver Name'.tr}: ${rideLifecycle.driverName}\n"; + message += + "* ${'Car'.tr}: ${rideLifecycle.make} - ${rideLifecycle.model} - ${rideLifecycle.licensePlate}\n"; + message += "* ${'Phone'.tr}: ${rideLifecycle.driverPhone}\n\n"; + + message += + "${'Location'.tr}: https://www.google.com/maps/search/?api=1&query=${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}\n"; + message += "Please help! Contact me as soon as possible.".tr; + + launchCommunication( + 'whatsapp', box.read(BoxName.sosPhonePassenger), message); + } + + String formatSyrianPhone(String phone) { + phone = phone.replaceAll(' ', '').replaceAll('+', ''); + if (phone.startsWith('00963')) { + phone = phone.replaceFirst('00963', '963'); + } + if (phone.startsWith('0963')) { + phone = phone.replaceFirst('0963', '963'); + } + if (phone.startsWith('963')) { + return phone; + } + if (phone.startsWith('09')) { + return '963' + phone.substring(1); + } + if (phone.startsWith('9') && phone.length == 9) { + return '963' + phone; + } + return phone; + } + + String formatSyrianPhoneNumber(String phoneNumber) { + String trimmedPhone = phoneNumber.trim(); + if (trimmedPhone.startsWith('09')) { + return '963${trimmedPhone.substring(1)}'; + } + if (trimmedPhone.startsWith('963')) { + return trimmedPhone; + } + return '963$trimmedPhone'; + } + + void sendSMS(String to) async { + final rideLifecycle = Get.find(); + String formattedDriverPhone = + rideLifecycle.driverPhone.replaceAll(' ', '').replaceAll('+', ''); + + String message = + 'Hi! This is ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n I am using ${box.read(AppInformation.appName)} to ride with ${rideLifecycle.passengerName} as the driver. ${rideLifecycle.passengerName} \nis driving a ${rideLifecycle.model}\n with license plate ${rideLifecycle.licensePlate}.\n I am currently located at ${Get.find().passengerLocation}.\n If you need to reach me, please contact the driver directly at\n\n $formattedDriverPhone.'; + + launchCommunication('sms', to, message); + } + + void sendWhatsapp(String to) async { + final rideLifecycle = Get.find(); + final locSearch = Get.find(); + String formattedPhone = formatSyrianPhone(to); + + String message = + '${'${'Hi! This is'.tr} ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n${' I am using'.tr}'} ${AppInformation.appName}${' to ride with'.tr} ${rideLifecycle.passengerName}${' as the driver.'.tr} ${rideLifecycle.passengerName} \n${'is driving a '.tr}${rideLifecycle.model}\n${' with license plate '.tr}${rideLifecycle.licensePlate}.\n${' I am currently located at '.tr} https://www.google.com/maps/place/${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}.\n${' If you need to reach me, please contact the driver directly at'.tr}\n\n ${rideLifecycle.driverPhone}.'; + + launchCommunication('whatsapp', formattedPhone, message); + } + + Future driverArrivePassengerDialoge() { + final rideLifecycle = Get.find(); + return Get.defaultDialog( + barrierDismissible: false, + title: 'Hi ,I Arrive your location'.tr, + titleStyle: AppStyle.title, + middleText: 'Please go to Car Driver'.tr, + middleTextStyle: AppStyle.title, + confirm: MyElevatedButton( + title: 'Ok I will go now.'.tr, + onPressed: () { + NotificationService.sendNotification( + target: rideLifecycle.driverToken.toString(), + title: 'Hi ,I will go now'.tr, + body: 'I will go now'.tr, + isTopic: false, + tone: 'ding', + driverList: [], + category: 'Hi ,I will go now', + ); + Get.back(); + rideLifecycle.remainingTime = 0; + rideLifecycle.update(); + }, + ), + ); + } + + void getDialog(String title, String? midTitle, VoidCallback onPressed) { + final textToSpeechController = Get.find(); + Get.defaultDialog( + title: title, + titleStyle: AppStyle.title, + middleTextStyle: AppStyle.title, + content: Column( + children: [ + IconButton( + onPressed: () async { + await textToSpeechController.speakText(title ?? midTitle!); + }, + icon: const Icon(Icons.headphones), + ), + Text( + midTitle!, + style: AppStyle.title, + ) + ], + ), + confirm: MyElevatedButton( + title: 'Ok'.tr, + onPressed: onPressed, + kolor: AppColor.greenColor, + ), + cancel: MyElevatedButton( + title: 'Cancel', + kolor: AppColor.redColor, + onPressed: () { + Get.back(); + }, + ), + ); + } + + Future shareTripWithFamily() async { + _ensureSosNumber(() { + final rideLifecycle = Get.find(); + String storedPhone = box.read(BoxName.sosPhonePassenger)!; + + if (rideLifecycle.rideId == 'yet' || rideLifecycle.driverId.isEmpty) { + Get.snackbar("Alert".tr, "Wait for the trip to start first".tr); + return; + } + + var numberPhone = formatSyrianPhoneNumber(storedPhone); + String trackingLink = rideLifecycle.generateTrackingLink( + rideLifecycle.rideId, rideLifecycle.driverId); + + String message = """ +مرحباً، تابع رحلتي مباشرة على تطبيق انطلق 🚗 + +يمكنك تتبع مسار الرحلة من هنا: +$trackingLink + +السائق: ${rideLifecycle.passengerName} +السيارة: ${rideLifecycle.model} - ${rideLifecycle.licensePlate} +شكراً لاستخدامك انطلق! +""" + .tr; + + String messageEn = """Hello, follow my trip live on Intaleq 🚗 + +Track my ride here: +$trackingLink + +Driver: ${rideLifecycle.passengerName} +Car: ${rideLifecycle.model} - ${rideLifecycle.licensePlate} +Thank you for using Intaleq! +"""; + + String userLanguage = box.read(BoxName.lang) ?? 'ar'; + message = (userLanguage == 'ar') ? message : messageEn; + + Log.print("Sending WhatsApp to: $numberPhone"); + launchCommunication('whatsapp', numberPhone, message); + + box.write(BoxName.parentTripSelected, true); + update(); + }); + } + + Future getTokenForParent() async { + _ensureSosNumber(() async { + String storedPhone = box.read(BoxName.sosPhonePassenger)!; + var numberPhone = formatSyrianPhoneNumber(storedPhone); + Log.print("Searching for Parent Token with Phone: $numberPhone"); + + var res = await CRUD() + .post(link: AppLink.getTokenParent, payload: {'phone': numberPhone}); + + if (res is Map) { + handleResponse(res); + } else { + try { + var decoded = jsonDecode(res); + handleResponse(decoded); + } catch (e) { + Log.print("Error parsing parent response: $res"); + } + } + }); + } + + void handleResponse(Map res) { + final rideLifecycle = Get.find(); + if (res['status'] == 'failure') { + if (Get.isDialogOpen ?? false) Get.back(); + + Get.defaultDialog( + title: "No user found".tr, + titleStyle: AppStyle.title, + content: Column( + children: [ + Text( + "No passenger found for the given phone number".tr, + style: AppStyle.title, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + Text( + "Send Intaleq app to him".tr, + style: AppStyle.title + .copyWith(color: AppColor.greenColor, fontSize: 14), + textAlign: TextAlign.center, + ) + ], + ), + confirm: MyElevatedButton( + title: 'Send Invite'.tr, + onPressed: () { + Get.back(); + var rawPhone = box.read(BoxName.sosPhonePassenger); + if (rawPhone == null) return; + var phone = formatSyrianPhoneNumber(rawPhone); + + var message = '''Dear Friend, + +🚀 I have just started an exciting trip on Intaleq! +Download the app to track my ride: + +👉 Android: https://play.google.com/store/apps/details?id=com.Intaleq.intaleq&hl=en-US +👉 iOS: https://apps.apple.com/st/app/intaleq-rider/id6748075179 + +See you there! +Intaleq Team'''; + + launchCommunication('whatsapp', phone, message); + }, + ), + cancel: MyElevatedButton( + title: 'Cancel'.tr, + onPressed: () { + Get.back(); + }, + ), + ); + } else if (res['status'] == 'success') { + if (Get.isDialogOpen ?? false) Get.back(); + + Get.snackbar("Success".tr, "The invitation was sent successfully".tr, + backgroundColor: AppColor.greenColor, colorText: Colors.white); + + List tokensData = res['data']; + for (var device in tokensData) { + String tokenParent = device['token']; + + NotificationService.sendNotification( + category: "Trip Monitoring", + target: tokenParent, + title: "Trip Monitoring".tr, + body: "Click to track the trip".tr, + isTopic: false, + tone: 'tone1', + driverList: [rideLifecycle.rideId, rideLifecycle.driverId], + ); + box.write(BoxName.tokenParent, tokenParent); + } + box.write(BoxName.parentTripSelected, true); + } + } + + @override + void onClose() { + EmergencySignalService.instance.stopListening(); + super.onClose(); + } +} diff --git a/lib/controller/home/map_passenger_controller.dart b/lib/controller/home/map_passenger_controller.dart index fa2ada4..f5eadb7 100644 --- a/lib/controller/home/map_passenger_controller.dart +++ b/lib/controller/home/map_passenger_controller.dart @@ -1,7751 +1,7751 @@ -import 'dart:async'; -import 'package:Intaleq/services/offline_map_service.dart'; -import 'package:Intaleq/services/emergency_signal_service.dart'; -import 'package:Intaleq/views/widgets/mycircular.dart'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:math' show Random, atan2, cos, max, min, pi, pow, sin, sqrt; -import 'dart:math' as math; -import 'dart:ui'; -import 'dart:typed_data'; -import 'package:image/image.dart' as img; -import 'package:Intaleq/services/ride_live_notification.dart'; -import 'package:crypto/crypto.dart'; -import 'package:Intaleq/views/Rate/rate_captain.dart'; -import 'package:Intaleq/views/Rate/rating_driver_bottom.dart'; -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:http/http.dart' as http; - -import 'package:Intaleq/constant/univeries_polygon.dart'; -import 'package:Intaleq/controller/firebase/local_notification.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_confetti/flutter_confetti.dart' hide Circle; -import 'package:socket_io_client/socket_io_client.dart' as IO; -import 'package:vector_math/vector_math.dart' show radians; - -import 'package:Intaleq/controller/functions/tts.dart'; -import 'package:Intaleq/views/home/map_page_passenger.dart'; -import 'package:Intaleq/views/widgets/my_textField.dart'; -import 'package:flutter/material.dart'; -import 'package:geolocator/geolocator.dart'; -import 'package:get/get.dart'; -import 'package:intaleq_maps/intaleq_maps.dart'; -// import 'package:google_polyline_algorithm/google_polyline_algorithm.dart'; -import 'package:intl/intl.dart'; -import 'package:location/location.dart'; -import 'package:Intaleq/constant/country_polygons.dart'; -import 'package:Intaleq/constant/links.dart'; -import 'package:Intaleq/constant/style.dart'; -import 'package:Intaleq/controller/home/points_for_rider_controller.dart'; -import 'package:Intaleq/views/home/map_widget.dart/form_serch_multiy_point.dart'; -import '../../constant/api_key.dart'; -import '../../constant/box_name.dart'; -import '../../constant/colors.dart'; -import '../../constant/info.dart'; -import '../../constant/table_names.dart'; -import '../../env/env.dart'; -import '../../main.dart'; -import '../../models/model/locations.dart'; -import '../../models/model/painter_copoun.dart'; -import '../../print.dart'; -import '../../services/pip_service.dart'; -import '../../views/home/map_widget.dart/cancel_raide_page.dart'; -import '../../views/home/map_widget.dart/car_details_widget_to_go.dart'; -import '../../views/home/map_widget.dart/select_driver_mishwari.dart'; -import '../../views/widgets/elevated_btn.dart'; -import '../../views/widgets/error_snakbar.dart'; -import '../../views/widgets/mydialoug.dart'; -import '../firebase/firbase_messge.dart'; -import '../firebase/notification_service.dart'; -import '../functions/audio_record1.dart'; -import '../functions/crud.dart'; -import '../functions/launch.dart'; -import '../functions/package_info.dart'; -import '../functions/secure_storage.dart'; -import '../payment/payment_controller.dart'; -import 'decode_polyline_isolate.dart'; -import 'deep_link_controller.dart'; -import 'device_performance.dart'; -import 'ios_live_activity_service.dart'; -import 'vip_waitting_page.dart'; - -enum RideState { - noRide, // لا يوجد رحلة جارية، عرض واجهة البحث - cancelled, // تم إلغاء الرحلة - preCheckReview, // يوجد رحلة منتهية، تحقق من التقييم - searching, // جاري البحث عن كابتن - driverApplied, // تم قبول الطلب - driverArrived, // وصل السائق - inProgress, // الرحلة بدأت بالفعل - finished, // انتهت الرحلة (سيتم تحويلها إلى preCheckReview) -} - -class MapPassengerController extends GetxController { - bool isLoading = true; - TextEditingController placeDestinationController = TextEditingController(); - TextEditingController increasFeeFromPassenger = TextEditingController(); - TextEditingController placeStartController = TextEditingController(); - TextEditingController wayPoint0Controller = TextEditingController(); - TextEditingController wayPoint1Controller = TextEditingController(); - TextEditingController wayPoint2Controller = TextEditingController(); - TextEditingController wayPoint3Controller = TextEditingController(); - TextEditingController wayPoint4Controller = TextEditingController(); - TextEditingController sosPhonePassengerProfile = TextEditingController(); - TextEditingController whatsAppLocationText = TextEditingController(); - TextEditingController messageToDriver = TextEditingController(); - final sosFormKey = GlobalKey(); - final promoFormKey = GlobalKey(); - final messagesFormKey = GlobalKey(); - final increaseFeeFormKey = GlobalKey(); - List data = []; - List bounds = []; - List placesStart = []; - List driversToken = []; - LatLng previousLocationOfDrivers = const LatLng(0, 0); - double angleDegrees = 0; - LatLng currentLocationOfDrivers = const LatLng(0, 0); - List allTextEditingPlaces = []; - List placesDestination = []; - List wayPoint0 = []; - List wayPoint1 = []; - List wayPoint2 = []; - List wayPoint3 = []; - List wayPoint4 = []; - final firebaseMessagesController = - Get.isRegistered() - ? Get.find() - : Get.put(FirebaseMessagesController()); - List> placeListResponseAll = []; - - List placeListResponse = [ - formSearchPlaces(0), - formSearchPlaces(1), - formSearchPlaces(2), - formSearchPlaces(3), - ]; - - IntaleqMapController? mapController; - bool isStyleLoaded = false; - - Set markers = {}; - Set polyLines = {}; - Set polygons = {}; - Set circles = {}; - double speed = 0; - PermissionStatus? permissionGranted; - - LatLngBounds? lastComputedBounds; - late LatLng passengerLocation = const LatLng(32, 34); - late LatLng newMyLocation = const LatLng(32.115295, 36.064773); - late LatLng newStartPointLocation = const LatLng(32.115295, 36.064773); - late LatLng newPointLocation0 = const LatLng(32.115295, 36.064773); - late LatLng newPointLocation1 = const LatLng(32.115295, 36.064773); - late LatLng newPointLocation2 = const LatLng(32.115295, 36.064773); - late LatLng newPointLocation3 = const LatLng(32.115295, 36.064773); - late LatLng newPointLocation4 = const LatLng(32.115295, 36.064773); - late LatLng myDestination; - List polylineCoordinates = []; - List polylineCoordinates0 = []; - List polylineCoordinates1 = []; - List polylineCoordinates2 = []; - List polylineCoordinates3 = []; - List polylineCoordinates4 = []; - List> polylineCoordinatesPointsAll = []; - List carsLocationByPassenger = []; - List driverCarsLocationToPassengerAfterApplied = []; - String markerIcon = "marker_icon"; - String tripIcon = "trip_icon"; - String startIcon = "start_icon"; - String endIcon = "end_icon"; - String carIcon = "car_icon"; - String motoIcon = "moto_icon"; - String ladyIcon = "lady_icon"; - double height = 150; - DateTime currentTime = DateTime.now(); - final location = Location(); - late LocationData currentLocation; - double heightMenu = 0; - double widthMenu = 0; - double heightPickerContainer = 90; - double heightPointsPageForRider = 0; - double mainBottomMenuMapHeight = Get.height * .2; - double wayPointSheetHeight = 0; - String stringRemainingTimeToPassenger = ''; - String stringRemainingTimeDriverWaitPassenger5Minute = ''; - bool isDriverInPassengerWay = false; - bool isDriverArrivePassenger = false; - bool startLocationFromMap = false; - bool isAnotherOreder = false; - bool isWhatsAppOrder = false; - bool passengerStartLocationFromMap = false; - bool workLocationFromMap = false; - bool homeLocationFromMap = false; - bool isPassengerRideLocationWidget = false; - bool startLocationFromMap0 = false; - bool startLocationFromMap1 = false; - bool startLocationFromMap2 = false; - bool startLocationFromMap3 = false; - bool startLocationFromMap4 = false; - List startLocationFromMapAll = []; - double latePrice = 0; - double fuelPrice = 0; - double heavyPrice = 0; - double naturePrice = 0; - bool heightMenuBool = false; - String statusRide = 'wait'; - String statusRideVip = 'wait'; - bool statusRideFromStart = false; - bool isPickerShown = false; - bool isPointsPageForRider = false; - bool isBottomSheetShown = false; - bool mapType = false; - bool reloadStartApp = false; - bool mapTrafficON = false; - bool isCancelRidePageShown = false; - bool isCashConfirmPageShown = false; - bool isPaymentMethodPageShown = false; - bool isRideFinished = false; - bool rideConfirm = false; - bool isMarkersShown = false; - bool isMainBottomMenuMap = true; - - int durationToPassenger = 0; - bool isWayPointSheet = false; - bool isWayPointStopsSheet = false; - bool isWayPointStopsSheetUtilGetMap = false; - double heightBottomSheetShown = 0; - double cashConfirmPageShown = 250; - late String driverId = ''; - late String gender = ''; - double widthMapTypeAndTraffic = 50; - double paymentPageShown = Get.height * .6; - late LatLng southwest; - late LatLng northeast; - List carLocationsModels = []; - var dataCarsLocationByPassenger; - var datadriverCarsLocationToPassengerAfterApplied; - CarLocation? nearestCar; - - bool shouldFetch = true; // Flag to determine if fetch should be executed - int selectedPassengerCount = 1; - double progress = 0; - double progressTimerToPassengerFromDriverAfterApplied = 0; - double progressTimerDriverWaitPassenger5Minute = 0; - int durationTimer = 9; - int durationToRide = 0; - int remainingTime = 25; - int remainingTimeToPassengerFromDriverAfterApplied = 60; - int remainingTimeDriverWaitPassenger5Minute = 60; - int timeToPassengerFromDriverAfterApplied = 0; - Timer? timerToPassengerFromDriverAfterApplied; - bool rideTimerBegin = false; - double progressTimerRideBegin = 0; - int remainingTimeTimerRideBegin = 60; - String stringRemainingTimeRideBegin = ''; - String hintTextStartPoint = 'Search for your Start point'.tr; - String hintTextwayPoint0 = 'Search for waypoint'.tr; - String hintTextwayPoint1 = 'Search for waypoint'.tr; - String hintTextwayPoint2 = 'Search for waypoint'.tr; - String hintTextwayPoint3 = 'Search for waypoint'.tr; - String hintTextwayPoint4 = 'Search for waypoint'.tr; - String currentLocationString = 'Current Location'.tr; - String currentLocationString0 = 'Current Location'.tr; - String currentLocationString1 = 'Add Location 1'.tr; - String currentLocationString2 = 'Add Location 2'.tr; - String currentLocationString3 = 'Add Location 3'.tr; - String currentLocationString4 = 'Add Location 4'.tr; - String placesCoordinate0 = ''.tr; - String placesCoordinate1 = ''.tr; - String placesCoordinate2 = ''.tr; - String placesCoordinate3 = ''.tr; - String placesCoordinate4 = ''.tr; - List currentLocationStringAll = []; - List hintTextwayPointStringAll = []; - var placesCoordinate = []; - String hintTextDestinationPoint = 'Select your destination'.tr; - late String rideId = 'yet'; - bool noCarString = false; - bool isCashSelectedBeforeConfirmRide = false; - bool isPassengerChosen = false; - bool isSearchingWindow = false; - bool currentLocationToFormPlaces = false; - bool currentLocationToFormPlaces0 = false; - bool currentLocationToFormPlaces1 = false; - bool currentLocationToFormPlaces2 = false; - bool currentLocationToFormPlaces3 = false; - bool currentLocationToFormPlaces4 = false; - List currentLocationToFormPlacesAll = []; - - // ── Multi-Waypoint (max 2 stops) ────────────────────────────────────────── - List menuWaypoints = [null, null]; - List menuWaypointNames = ['', '']; - int activeMenuWaypointCount = 0; - bool isPickingWaypoint = false; - int pickingWaypointIndex = -1; - - late String driverToken = ''; - int carsOrder = 0; - int wayPointIndex = 0; - late double kazan = 8; - String? mapAPIKEY; - late double totalME = 0; - late double tax = 0; - late double totalPassenger = 0; - late double totalCostPassenger = 0; - late double totalPassengerComfort = 0; - late double totalPassengerComfortDiscount = 0; - late double totalPassengerElectricDiscount = 0; - late double totalPassengerLadyDiscount = 0; - late double totalPassengerSpeedDiscount = 0; - late double totalPassengerBalashDiscount = 0; - late double totalPassengerRaihGaiDiscount = 0; - late double totalPassengerScooter = 0; - late double totalPassengerVan = 0; - late double totalDriver = 0; - late double averageDuration = 0; - late double costDuration = 0; - late double costDistance = 0; - late double distance = 0; - late double duration = 0; - bool _isDriverAppliedLogicExecuted = false; // فلاج لمنع التنفيذ المتكرر - bool _isDriverArrivedLogicExecuted = false; - bool _isRideBeginLogicExecuted = false; - DateTime? _searchStartTime; // لتتبع مدة البحث - DateTime? _lastDriversNotifyTime; // لتتبع آخر مرة تم إرسال إشعار للسائقين - final int _masterTimerIntervalSeconds = 5; // فاصل زمني ثابت للمؤقت الرئيسي - final int _searchTimeoutSeconds = 60; // مهلة البحث قبل عرض خيار زيادة السعر - final int _notifyDriversIntervalSeconds = - 25; // إرسال إشعار للسائقين كل 25 ثانية - // متغير لمنع أي عمليات تحديث أثناء التقييم - bool _isRatingScreenOpen = false; - // --- إضافة جديدة: متغيرات لإدارة البحث المتوسع --- - int _currentSearchPhase = 0; // لتتبع المرحلة الحالية للبحث - bool _isFetchingDriverLocation = false; // متغير لمنع تكرار الطلب - -// === استبدل initSocket بالكامل === - late IO.Socket socket; - bool isSocketConnected = false; - int _reconnectAttempts = 0; - final int _maxReconnectAttempts = 5; - Timer? _reconnectTimer; - var currentRideId; -// لتخزين نقاط مسار السائق الحالية للمقارنة - List _currentDriverRoutePoints = []; - -// متغير لتتبع مصدر القبول — Socket أم غيره - String _rideAcceptedViaSource = "Unknown"; -// عدّاد تحديثات الموقع المستلمة من السوكيت (لقياس الصحة) - int _socketLocationUpdatesCount = 0; - - final Map _pollingIntervals = { - RideState.noRide: 6, - RideState.searching: 8, - RideState.driverApplied: 10, - RideState.driverArrived: 15, - RideState.inProgress: 15, - RideState.cancelled: 3600, - RideState.finished: 3600, - RideState.preCheckReview: 3600, - }; - // لمنع التكرار (عشان ما يعمل 100 طلب في نفس اللحظة) - bool _isRecalculatingRoute = false; -// متغير لمراقبة صحة السوكيت - DateTime? _lastSocketLocationTime; - // مسافة السماحية (مثلاً 150 متر) قبل اعتبار السائق "خارج المسار" - final double _deviationThresholdMeters = 150.0; -// ... (باقي الـ Imports) - -// متغيرات التحكم - Timer? _locationPollingTimer; // تايمر مخصص للموقع فقط - -// ============================================================================== -// 1. الدالة الرئيسية لتأسيس الاتصال (تستدعى عند بدء البحث startSearchingForDriver) -// ============================================================================== - Timer? _heartbeatTimer; - void initConnectionWithSocket() { - if (isSocketConnected && socket != null) return; - - String passengerId = box.read(BoxName.passengerID).toString(); - Log.print("🔌 Initializing Socket for Passenger: $passengerId"); - - socket = IO.io( - AppLink.serverSocket, - IO.OptionBuilder() - .setTransports(['websocket']) - .disableAutoConnect() - .setQuery({'id': passengerId}) - // ✅ [FIX] إعادة اتصال شبه-لانهائية (999 محاولة) بدلاً من 20 - .setReconnectionAttempts(20) - // ✅ [FIX] تأخير أقل (1.5 ثانية) مع حد أقصى (8 ثواني) للتسريع - .setReconnectionDelay(1500) - .setReconnectionDelayMax(8000) - .enableReconnection() - .setExtraHeaders({'Connection': 'Upgrade'}) - .build(), - ); - - socket.connect(); - - // ✅ معالج الاتصال الأول - socket.onConnect((_) { - Log.print("✅ Socket Connected Successfully"); - isSocketConnected = true; - _reconnectAttempts = 0; - _startHeartbeat(); - - // ✅ [FIX] الاشتراك مجدداً في أحداث الرحلة عند كل اتصال - if (rideId != null && rideId != 'yet' && driverId.isNotEmpty) { - socket.emit('subscribe_driver_location', { - 'ride_id': rideId, - 'driver_id': driverId, - }); - Log.print("📡 Re-subscribed to driver location after connect"); - } - - update(); - }); - - // ⚠️ معالج الانقطاع - socket.onDisconnect((_) { - Log.print("⚠️ Socket Disconnected — Auto-Reconnect will handle it"); - isSocketConnected = false; - - // تفعيل Polling أسرع كـ Fallback مؤقت (سيتم إيقافه عند عودة الاتصال) - if (_isActiveRideState()) { - Log.print("🔄 Enabling Fast Polling Fallback (4s) until reconnect..."); - _startMasterTimerWithInterval(4); - } - update(); - }); - - // 🔁 [FIX] معالج إعادة الاتصال الناجحة - socket.onReconnect((_) { - Log.print("🔁 Socket Reconnected Successfully!"); - isSocketConnected = true; - _reconnectAttempts = 0; - - // استئناف النبضة فوراً - _startHeartbeat(); - - // إعادة الاشتراك في أحداث الرحلة - if (rideId != null && rideId != 'yet' && driverId.isNotEmpty) { - socket.emit('subscribe_driver_location', { - 'ride_id': rideId, - 'driver_id': driverId, - }); - Log.print("📡 Re-subscribed to driver location after reconnect"); - } - - // ✅ [FIX] إيقاف الـ Fast Polling لأن السوكيت عاد - if (_isActiveRideState()) { - Log.print("✅ Socket back online — stopping Fast Polling Fallback"); - _masterTimer?.cancel(); - _masterTimer = null; - } - - update(); - }); - - // 🔄 [FIX] معالج محاولات إعادة الاتصال (للتشخيص) - socket.onReconnectAttempt((attemptNumber) { - Log.print("🔄 Socket Reconnect Attempt #$attemptNumber..."); - }); - - // ❌ معالج الأخطاء - socket.onError((error) { - Log.print("❌ Socket Error: $error"); - isSocketConnected = false; - }); - - // 📩 معالج تحديثات الحالة - socket.on('ride_status_change', (data) { - Log.print("📩 Socket Event: ride_status_change -> $data"); - _handleRideStatusChangeWithSocket(data); - }); - - // 📍 معالج موقع السائق - socket.on('driver_location_update', (data) { - handleDriverLocationUpdate(data); - }); - } - - void _startHeartbeat() { - _heartbeatTimer?.cancel(); - _heartbeatTimer = Timer.periodic(const Duration(seconds: 25), (timer) { - if (isSocketConnected && socket.connected) { - socket.emit('heartbeat', - {'passenger_id': box.read(BoxName.passengerID).toString()}); - } - }); - } - -// دالة مساعدة - bool _isActiveRideState() { - return currentRideState.value == RideState.searching || - currentRideState.value == RideState.driverApplied || - currentRideState.value == RideState.driverArrived || - currentRideState.value == RideState.inProgress; - } - - /// فحص سريع: هل السوكيت يعمل ويرسل بيانات؟ - bool _isSocketHealthy() { - if (!isSocketConnected) return false; - if (_lastSocketLocationTime == null) return false; - final diff = DateTime.now().difference(_lastSocketLocationTime!).inSeconds; - return diff < 20; // إذا آخر تحديث قبل أقل من 20 ثانية - } - - /// 🧠 خوارزمية ذكية: حساب أقصر مسافة بين موقع السائق والـ Polyline (بدون API) - double _calculateDistanceToPolyline(LatLng point, List polyline) { - if (polyline.isEmpty) return 999.0; - double minDistance = double.infinity; - - for (int i = 0; i < polyline.length - 1; i++) { - double d = _distToSegment(point, polyline[i], polyline[i + 1]); - if (d < minDistance) minDistance = d; - } - return minDistance; - } - - double _distToSegment(LatLng p, LatLng v, LatLng w) { - double l2 = _dist2(v, w); - if (l2 == 0) return _distanceBetween(p, v); - double t = ((p.latitude - v.latitude) * (w.latitude - v.latitude) + - (p.longitude - v.longitude) * (w.longitude - v.longitude)) / - l2; - t = max(0, min(1, t)); - return _distanceBetween( - p, - LatLng(v.latitude + t * (w.latitude - v.latitude), - v.longitude + t * (w.longitude - v.longitude))); - } - - double _dist2(LatLng v, LatLng w) { - return pow(v.latitude - w.latitude, 2).toDouble() + - pow(v.longitude - w.longitude, 2).toDouble(); - } - - double _distanceBetween(LatLng p1, LatLng p2) { - return Geolocator.distanceBetween( - p1.latitude, p1.longitude, p2.latitude, p2.longitude); - } - -// ============================================================================== -// 2. العقل المدبر: توجيه الحالات -// ============================================================================== - void _handleRideStatusChangeWithSocket(dynamic data) { - if (data == null || data['status'] == null) return; - - String newStatus = data['status'].toString().toLowerCase(); - Log.print("🔔 Socket Status Update: $newStatus"); -// استخراج بيانات السائق إذا توفرت (تأتي من acceptRide.php) - Map? driverInfo; - if (data['driver_info'] != null && data['driver_info'] is Map) { - driverInfo = Map.from(data['driver_info']); - } - switch (newStatus) { - case 'accepted': // أو apply/applied حسب تسمية السيرفر - _onDriverAcceptedWithSocket(data, driverData: driverInfo); - break; - - case 'arrived': - _onDriverArrivedWithSocket(); - break; - - case 'started': // أو begin - _onRideStartedWithSocket(); - break; - - case 'finished': // أو ended - _onRideFinishedWithSocket(data); - break; - - case 'cancelled': - _onRideCancelledWithSocket(data); - break; - - case 'no_drivers_found': - showNoDriverDialog(); - break; - } - } - -// ============================================================================== -// 3. دوال المعالجة التفصيلية (Actions) -// ============================================================================== - void showNoDriverDialog() { - Get.defaultDialog( - title: "No Drivers Found".tr, - middleText: - "Sorry, there are no cars available of this type right now.".tr, - textConfirm: "Refresh Map".tr, - textCancel: "Cancel".tr, - confirmTextColor: Colors.white, - onConfirm: () { - Get.back(); // إغلاق الديالوج - restCounter(); - stopAllTimers(); - Get.offAll(() => MapPagePassenger()); // إعادة تحميل صفحة الخريطة - }, - ); - } - -// أ) عند قبول السائق للرحلة - // أ) عند قبول السائق للرحلة (معدلة) - // دالة الاستقبال من السوكيت (تصبح مجرد محول) - void _onDriverAcceptedWithSocket(dynamic data, - {Map? driverData}) { - // استخراج البيانات وتمريرها للدالة الموحدة - Map? info = driverData; - - // دعم الهيكلية الجديدة - if (info == null && data['driver_info'] != null) { - info = Map.from(data['driver_info']); - } - // دعم الهيكلية القديمة (إن وجدت) - else if (info == null && data['driverList'] != null) { - // تحويل driverList إلى map إذا لزم الأمر - } - - processRideAcceptance(driverData: info, source: "Socket"); - } - - void _fillDriverDataLocally(Map data) { - try { - // تعبئة المتغيرات بناءً على أسماء الحقول في acceptRide.php - driverId = data['driver_id']?.toString() ?? ''; - driverPhone = data['phone']?.toString() ?? ''; - - String fName = data['first_name']?.toString() ?? ''; - String lName = data['last_name']?.toString() ?? ''; - passengerName = lName.isNotEmpty - ? "$fName $lName" - : fName; // (هنا المتغير اسمه passengerName لكنه يحمل اسم السائق في الكود لديك) - driverName = passengerName; - - make = data['make']?.toString() ?? ''; - model = data['model']?.toString() ?? ''; - carColor = data['color']?.toString() ?? ''; - colorHex = data['color_hex']?.toString() ?? ''; - licensePlate = data['car_plate']?.toString() ?? ''; - carYear = data['year']?.toString() ?? ''; - - driverRate = data['ratingDriver']?.toString() ?? '5.0'; - driverToken = data['token']?.toString() ?? ''; - - // إذا كان هناك أي بيانات أخرى تحتاجها الواجهة - update(); - } catch (e) { - Log.print("Error parsing socket driver data: $e"); - } - } - -// دالة موحدة: تجلب المسار + الوقت + المسافة + ترسم الخط + تضبط الكاميرا - Future calculateDriverToPassengerRoute( - LatLng driverPos, LatLng passengerPos) async { - // 1. تجهيز الرابط (نفس API الـ Direction) - // نستخدم overview=full للحصول على الرسمة، و steps=false لتخفيف البيانات - final Map queryParams = { - 'fromLat': driverPos.latitude.toString(), - 'fromLng': driverPos.longitude.toString(), - 'toLat': passengerPos.latitude.toString(), - 'toLng': passengerPos.longitude.toString(), - }; - final uri = - Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams); - - Log.print('📍 Calculating Driver Route: $uri'); - - try { - final response = await http.get(uri, headers: { - 'x-api-key': Env.mapSaasKey, - }).timeout(const Duration(seconds: 20)); - - if (response.statusCode == 200) { - final responseData = json.decode(response.body); - - // Support both old format (routes[0]) and new SaaS format (top-level) - var routeData = responseData['routes'] != null - ? responseData['routes'][0] - : responseData; - - // 2. تحديث المتغيرات (المسافة والوقت) - double durationSecondsRaw = (routeData['duration'] as num).toDouble(); - int finalDurationSeconds = - (durationSecondsRaw * kDurationScalar).toInt(); - double distanceMeters = (routeData['distance'] as num).toDouble(); - - timeToPassengerFromDriverAfterApplied = finalDurationSeconds; - remainingTimeToPassengerFromDriverAfterApplied = finalDurationSeconds; - distanceByPassenger = distanceMeters.toStringAsFixed(0); - - // تحديث نصوص الواجهة - int minutes = (finalDurationSeconds / 60).floor(); - int seconds = finalDurationSeconds % 60; - stringRemainingTimeToPassenger = - '$minutes:${seconds.toString().padLeft(2, '0')}'; - - Log.print( - '✅ Driver Route Info: $minutes min, ${distanceMeters.toInt()} m'); - - // 3. معالجة الرسم (Polyline) - // SaaS uses 'points', OSRM uses 'geometry' - String pointsString = - routeData['points'] ?? routeData['geometry'] ?? ""; - if (pointsString.isNotEmpty) { - List decodedPoints = - await compute(decodePolylineIsolate, pointsString); - // حفظ نسخة للمقارنة - _currentDriverRoutePoints = decodedPoints; - // إزالة خط مسار السائق القديم فقط - polyLines = polyLines - .where((p) => p.polylineId.value != 'driver_route') - .toSet(); - - // إضافة الخط الجديد (بستايل مميز للسائق) - polyLines = { - ...polyLines, - Polyline( - polylineId: const PolylineId('driver_route'), - points: decodedPoints, - color: - const Color(0xFF333333), // لون مختلف عن مسار الرحلة الأساسي - width: 5, - ) - }; - } - - // 4. ضبط الكاميرا لتشمل السائق والراكب - _fitCameraToPoints(driverPos, passengerPos); - - update(); // تحديث واحد للكل - } - } catch (e) { - Log.print('❌ Error calculating driver route: $e'); - } - } - - Future _checkAndRecalculateIfDeviated(LatLng driverPos) async { - // 1. شروط الخروج السريع - if (_isRecalculatingRoute || _currentDriverRoutePoints.isEmpty) return; - - // 2. حساب المسافة لأقرب نقطة في المسار (خوارزمية سريعة) - // نستخدم مكتبة Geolocator أو حساب رياضي بسيط - double minDistance = 100000.0; - - // لتقليل الحمل، لا نفحص كل النقاط، نفحص عينة (كل 5 نقاط مثلاً) أو الكل إذا المسار قصير - for (var point in _currentDriverRoutePoints) { - double dist = Geolocator.distanceBetween(driverPos.latitude, - driverPos.longitude, point.latitude, point.longitude); - if (dist < minDistance) minDistance = dist; - } - - // 3. اتخاذ القرار - if (minDistance > _deviationThresholdMeters) { - Log.print("⚠️ Driver deviated ($minDistance m). Recalculating route..."); - - _isRecalculatingRoute = true; - - // إعادة حساب المسار من موقع السائق الجديد - await calculateDriverToPassengerRoute(driverPos, passengerLocation); - - _isRecalculatingRoute = false; - } - } - -// ب) عند وصول السائق - void _onDriverArrivedWithSocket() { - Log.print("🚖 Driver Arrived (Socket)"); - - processDriverArrival("Socket"); - } - -// ج) عند بدء الرحلة - void _onRideStartedWithSocket() { - Log.print("🚀 Ride Started (Socket)"); - processRideBegin(source: "Socket"); - } - -// ربط السوكيت - // د) عند انتهاء الرحلة (Socket Listener) - void _onRideFinishedWithSocket(dynamic data) { - Log.print("🏁 Ride Finished (Socket)"); - - // نحاول استخراج DriverList من البيلود القادم من PHP - // في finish_ride_updates.php أسميناه 'DriverList' - var rawList = data['DriverList']; - - List listToSend = []; - - if (rawList != null) { - if (rawList is List) { - listToSend = rawList; - } else if (rawList is String) { - // احتياطاً لو وصل كنص - try { - listToSend = jsonDecode(rawList); - } catch (e) { - Log.print("Error: $e"); - } - } - } - - // إذا كانت القائمة فارغة، نحاول بناءها من البيانات المتفرقة (Fallback) - if (listToSend.isEmpty && data['price'] != null) { - listToSend = [ - driverId, // 0 - rideId, // 1 - driverToken, // 2 - data['price'].toString() // 3 - ]; - } - - // استدعاء المعالج الموحد - processRideFinished(listToSend, source: "Socket"); - } - -// هـ) عند الإلغاء - void _onRideCancelledWithSocket(dynamic data) { - processRideCancelledByDriver(data, source: "Socket"); - } - -// ============================================================================== -// 4. إدارة تتبع الموقع (Polling) - مفصولة عن السوكيت -// ============================================================================== -// متغير لمنع التكرار (Race Condition Guard) - bool _isCancelProcessed = false; - - /// **معالجة إلغاء الرحلة الموحدة (Gatekeeper)** - /// - /// تستدعى من [Socket] أو [FCM] عند قيام السائق بإلغاء الرحلة. - /// تضمن عدم تضارب الإشعارات وتوحد تجربة المستخدم. - Future processRideCancelledByDriver(dynamic data, - {String source = "Unknown"}) async { - if (_isCancelProcessed) return; - - _isCancelProcessed = true; - stopAllTimers(); - if (Get.isDialogOpen == true) Get.back(); - await RideLiveNotification.cancel(); - IosLiveActivityService.endRideActivity(); // ✅ أضف هذا السطر - PipService.disablePip(); // ✅ إيقاف PiP عند انتهاء الرحلة - if (Get.isDialogOpen == true) Get.back(); - await RideLiveNotification.cancel(); - Get.defaultDialog( - title: "Sorry 😔".tr, // استخدام المفتاح الإنجليزي - titleStyle: - const TextStyle(color: Colors.red, fontWeight: FontWeight.bold), - barrierDismissible: false, - content: Column( - children: [ - const Icon(Icons.cancel_presentation, - size: 50, color: Colors.redAccent), - const SizedBox(height: 10), - Text( - "The driver cancelled the trip for an emergency reason.\nDo you want to search for another driver immediately?" - .tr, - textAlign: TextAlign.center, - ), - ], - ), - actions: [ - TextButton( - onPressed: () { - Get.back(); - handleNoDriverFound(); - }, - child: Text("Cancel Trip".tr, - style: const TextStyle(color: Colors.grey)), - ), - ElevatedButton.icon( - style: - ElevatedButton.styleFrom(backgroundColor: AppColor.primaryColor), - icon: const Icon(Icons.refresh, color: Colors.white), - label: Text("Search for another driver".tr, - style: const TextStyle(color: Colors.white)), - onPressed: () { - Get.back(); - retrySearchForDrivers(); - }, - ), - ], - ); - } - - Future handleNoDriverFound() async { - stopAllTimers(); - await RideLiveNotification.cancel(); - IosLiveActivityService.endRideActivity(); // ✅ أضف هذا السطر - PipService.disablePip(); // ✅ إيقاف PiP - _isCancelProcessed = false; - currentRideState.value = RideState.noRide; - resetAllMapStates(); - Get.offAll(() => const MapPagePassenger()); - - Get.defaultDialog( - title: "We apologize 😔".tr, - middleText: "No drivers found at the moment.\nPlease try again later.".tr, - confirm: ElevatedButton( - onPressed: () => Navigator.pop(Get.context!), - child: Text("Ok".tr), - ), - ); - } - - /// **إعادة البحث عن سائقين (Retry Search)** - /// - /// تقوم باستدعاء السكربت لإعادة تفعيل الرحلة وبدء عداد البحث من جديد. - void retrySearchForDrivers() async { - _isCancelProcessed = false; - isSearchingWindow = true; - currentRideState.value = RideState.searching; - driversStatusForSearchWindow = 'Searching for nearby drivers...'.tr; - update(); - - try { - Log.print("🔄 Retrying search for ride ID: $rideId"); - - // تجهيز البيانات المخزنة للإرسال - var payload = { - "ride_id": rideId.toString(), - "passenger_id": box.read(BoxName.passengerID).toString(), - "passenger_name": box.read(BoxName.name).toString(), - "passenger_phone": box.read(BoxName.phone).toString(), - "passenger_email": box.read(BoxName.email).toString(), - "passenger_token": box.read(BoxName.tokenFCM).toString(), - "passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(), - "passenger_rating": "5.0", // أو قراءة التقييم الحقيقي إن وجد - - // قراءة البيانات من المتغيرات المحفوظة في الكنترولر أو الـ Box - "start_lat": startLocation.latitude.toString(), - "start_lng": startLocation.longitude.toString(), - "end_lat": endLocation.latitude.toString(), - "end_lng": endLocation.longitude.toString(), - "start_name": startNameAddress, - "end_name": endNameAddress, - "distance": distance.toString(), - "distance_text": distanceByPassenger ?? "", - "duration_text": durationToPassenger.toString(), - "price": totalPassenger.toString(), - "price_for_driver": costForDriver.toString(), - "car_type": box.read(BoxName.carType).toString(), - "is_wallet": Get.find().isWalletChecked.toString(), - - // الخطوات (اختياري) - "has_steps": Get.find().wayPoints.length > 1 - ? "true" - : "false", - // يمكنك إضافة الخطوات إذا كانت لديك في مصفوفة - }; - - var response = await CRUD().post( - link: "${AppLink.rideServerSide}/rides/retry_search_drivers.php", - payload: payload, - ); - - if (response['status'] == 'success') { - Log.print("✅ Search reset successfully."); - startSearchingTimer(); - } else { - mySnackbarWarning("Failed to search, please try again later".tr); - handleNoDriverFound(); - } - } catch (e) { - Log.print("Error retrying search: $e"); - mySnackbarWarning("Please check your internet connection".tr); - handleNoDriverFound(); - } - } - - Timer? _searchTimer; - - /// **بدء مؤقت البحث (Search Timer)** - /// - /// يبدأ عداداً (مثلاً 90 ثانية). إذا لم يتم قبول الرحلة خلال هذه المدة، - /// يتم إنهاء البحث واستدعاء [handleNoDriverFound]. - Future startSearchingTimer() async { - _searchTimer?.cancel(); - int seconds = 0; - - Log.print("⏳ Search Timer Started (90s)..."); - await RideLiveNotification.showSearching(driversStatusForSearchWindow); - - _searchTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - seconds++; - - // إذا تغيرت الحالة (مثلاً سائق قبل الرحلة)، نوقف العداد - if (currentRideState.value != RideState.searching) { - timer.cancel(); - return; - } - - // انتهاء الوقت (90 ثانية) - if (seconds >= 90) { - timer.cancel(); - handleNoDriverFound(); // ⏳ انتهى الوقت ولم نجد سائقاً - } - }); - } - -// متغير لمنع التكرار (Race Condition Guard) - bool _isArrivalProcessed = false; - - /// **معالجة وصول السائق الموحدة (Unified Driver Arrival Handler)** - /// - /// تقوم هذه الدالة بإدارة حدث وصول السائق إلى موقع الراكب، وتعمل كـ "حارس بوابة" (Gatekeeper) - /// لتوحيد المصادر سواء كانت من **WebSocket** أو **Firebase Notification**. - /// - /// **المهام التي تقوم بها:** - /// 1. **منع التضارب (Race Condition Guard):** تتأكد أن الحدث لم يتم تنفيذه مسبقاً لتجنب تكرار الإشعارات أو إعادة ضبط العدادات. - /// 2. **تحديث الحالة:** تغير حالة الرحلة فوراً إلى [RideState.driverArrived]. - /// 3. **تفعيل الواجهة:** تظهر ديالوج "السائق وصل" وتبدأ عداد الانتظار المجاني (5 دقائق). - /// - /// * [source] : نص يوضح مصدر الاستدعاء (مثل "Socket" أو "FCM") لأغراض التتبع (Logging). - Future processDriverArrival(String source) async { - // 1. الحارس: إذا تم التنفيذ سابقاً، توقف - if (currentRideState.value == RideState.driverArrived || - _isArrivalProcessed) { - Log.print("✋ Ignored Arrival from $source. Already processed."); - return; - } - - _isArrivalProcessed = true; - Log.print("🚖 Driver Arrived via $source! Processing..."); - - // 2. تحديث الحالة - currentRideState.value = RideState.driverArrived; - statusRide = 'Arrived'; - await RideLiveNotification.showDriverArrived(driverName ?? ''); - - // 3. تشغيل واجهة الوصول والعداد - driverArrivePassengerDialoge(); - startTimerDriverWaitPassenger5Minute(); - - // 4. إزالة مسار السائق واستعادة مسار الراكب للوجهة - if (polylineCoordinates.isNotEmpty) { - _playRouteAnimation(polylineCoordinates, lastComputedBounds); - } - - update(); - } - -// متغير لمنع التكرار - bool _isFinishProcessed = false; - - /// **معالجة إنهاء الرحلة الموحدة (Unified Ride Finish Handler)** - /// - /// تستدعى عند استلام حدث النهاية من السوكيت أو FCM. - /// تقوم بإغلاق السوكيت، إيقاف التتبع، والانتقال لشاشة التقييم والدفع. - /// - /// * [driverList]: قائمة البيانات [driverId, rideId, token, price]. - Future processRideFinished(List driverList, - {String source = "Unknown"}) async { - // 1. الحارس: منع التكرار - if (currentRideState.value == RideState.finished || _isFinishProcessed) { - Log.print("✋ Ignored Finish Request from $source. Already Finished."); - return; - } - - _isFinishProcessed = true; - Log.print( - "🏁 Ride Finished via $source. Price: ${driverList.length > 3 ? driverList[3] : 'N/A'}"); - - // 2. تحديث الحالة - currentRideState.value = RideState.finished; - - // 3. تنظيف الموارد - disposeRideSocket(); // إغلاق السوكيت - _stopDriverLocationPolling(); // إيقاف تتبع الموقع - if (Get.isRegistered()) { - Get.find().stopRecording(); - } - - // إغلاق أي ديالوج سابق - if (Get.isDialogOpen == true) Get.back(); - - // 4. إشعار "لا تنسى أغراضك" - NotificationController().showNotification( - 'Alert'.tr, - "Please make sure not to leave any personal belongings in the car.".tr, - 'tone1', - ); - IosLiveActivityService.endRideActivity(); - PipService.disablePip(); // ✅ إيقاف PiP - await RideLiveNotification.cancel(); - // 5. استخراج البيانات والانتقال - if (driverList.length >= 4) { - String price = driverList[3].toString(); - - // الانتقال لصفحة التقييم - Get.offAll(() => RateDriverFromPassenger(), arguments: { - 'driverId': driverList[0].toString(), - 'rideId': driverList[1].toString(), - 'price': price - }); - } - } - - void _startDriverLocationPollingWithTimer() { - Log.print("📍 Starting Driver Location Polling (6s interval)"); - - _locationPollingTimer?.cancel(); - - // استدعاء فوري لأول مرة - // getDriverCarsLocationToPassengerAfterApplied(); - - _locationPollingTimer = Timer.periodic(Duration(seconds: 6), (timer) { - // شرط التوقف: إذا انتهت الرحلة أو ألغيت - if (currentRideState.value == RideState.finished || - currentRideState.value == RideState.cancelled || - currentRideState.value == RideState.noRide) { - timer.cancel(); - return; - } - - // جلب الموقع وتحديث الماركر - getDriverCarsLocationToPassengerAfterApplied(); - }); - } - - void _stopDriverLocationPolling() { - Log.print("🛑 Stopping Location Polling"); - _locationPollingTimer?.cancel(); - _locationPollingTimer = null; - } - -// ============================================================================== -// 5. التنظيف والإغلاق -// ============================================================================== - void disposeRideSocket() { - if (socket != null) { - socket!.disconnect(); - socket!.dispose(); - // socket = null; - isSocketConnected = false; - Log.print("🔌 Socket Disposed"); - } - } - - /// ============================================================================== - /// 6. معالجة تحديث موقع السائق من السوكيت - /// ============================================================================== - void handleDriverLocationUpdate(dynamic data) { - if (!isSocketConnected || data == null) return; -// 🔥 1. تسجيل وقت استلام البيانات (تغذية الـ Watchdog) - _lastSocketLocationTime = DateTime.now(); - _socketLocationUpdatesCount++; - - // 🧠 إذا وصلتنا 3 تحديثات ناجحة من السوكيت، أوقف البولينج (إن كان يعمل) - if (_socketLocationUpdatesCount >= 3 && _locationPollingTimer != null) { - Log.print("✅ Socket delivering locations reliably. Stopping polling."); - _stopDriverLocationPolling(); - } - try { - double lat = double.tryParse(data['latitude']?.toString() ?? '0') ?? 0; - double lng = double.tryParse(data['longitude']?.toString() ?? '0') ?? 0; - double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0; - - if (lat == 0 || lng == 0) return; - - LatLng newPos = LatLng(lat, lng); - - // تحديث القائمة (نفس المنطق القديم) - if (driverCarsLocationToPassengerAfterApplied.isEmpty) { - driverCarsLocationToPassengerAfterApplied.add(newPos); - } else { - driverCarsLocationToPassengerAfterApplied[0] = newPos; - } - - currentLocationOfDrivers = newPos; - // ------------------------------------------------------------------ - // 🔥 2. Smart Rerouting Logic ( deviation > 50m ) - // ------------------------------------------------------------------ - if (_currentDriverRoutePoints.isNotEmpty) { - double deviation = - _calculateDistanceToPolyline(newPos, _currentDriverRoutePoints); - if (deviation > 50.0) { - Log.print( - "⚠️ Driver deviated by ${deviation.toStringAsFixed(1)}m. Smart Rerouting triggered."); - // إعادة رسم المسار محلياً (لا يتم استدعاؤه إلا عند الانحراف الحقيقي) - calculateDriverToPassengerRoute(newPos, passengerLocation); - } - } - - // 🔥 تحديث الكاميرا: ضمان بقاء السيارة في منتصف الخريطة - // ------------------------------------------------------------------ - // ملاحظة: تأكد من أن mapController قد تم تهيئته (initialized) - if (mapController != null) { - // نستخدم animateCamera لحركة ناعمة بدلاً من moveCamera القاسية - mapController!.animateCamera(CameraUpdate.newLatLng(newPos)); - } - // ------------------------------------------------------------------ - - // تحديث الوقت المتبقي (إذا أرسله السيرفر) - if (data['eta_seconds'] != null) { - int etaSeconds = int.tryParse(data['eta_seconds'].toString()) ?? 0; - if (etaSeconds > 0) { - remainingTimeToPassengerFromDriverAfterApplied = etaSeconds; - int minutes = (etaSeconds / 60).floor(); - int seconds = etaSeconds % 60; - stringRemainingTimeToPassenger = - '$minutes:${seconds.toString().padLeft(2, '0')}'; - } - } - - // تحديث الماركر - _updateDriverMarker(newPos, heading); - } catch (e) { - Log.print('Error in handleDriverLocationUpdate: $e'); - } - } - -// دالة مساعدة لتحديث ماركر السائق - void _updateDriverMarker(LatLng position, double heading) { - const String markerId = 'driver_location'; - final mId = MarkerId(markerId); - - final existingMarker = markers.cast().firstWhere( - (m) => m?.markerId == mId, - orElse: () => null, - ); - - if (existingMarker != null) { - _smoothlyUpdateMarker(existingMarker, position, heading, carIcon); - } else { - markers = { - ...markers, - Marker( - markerId: mId, - position: position, - icon: InlqBitmap.fromStyleImage(carIcon), - rotation: heading, - anchor: const Offset(0.5, 0.5), - ), - }; - update(); - } - } - -// === إضافة متغير للتحكم === - bool _isUsingFallback = false; - - void _startPollingFallback() { - if (_isUsingFallback) return; - - Log.print('🔄 Starting Polling Fallback Mode'); - _isUsingFallback = true; - - // استخدام _handleRideState الموجود (كما كان) - _startMasterTimer(); - } - - void handleActiveRideOnStartup(dynamic data) { - try { - if (data == null || data['has_active_ride'] != true) { - Log.print('[Startup] No active ride'); - currentRideState.value = RideState.noRide; - _startMasterTimer(); - return; - } - - Log.print('[Startup] ✅ Active ride found!'); - - var rideData = data['ride']; - rideId = rideData['ride_id'].toString(); - currentRideId = rideId; - driverId = rideData['driver_id']?.toString() ?? ''; - - String status = rideData['status']?.toString().toLowerCase() ?? ''; - - // تحديد الحالة - if (status == 'waiting' || status == 'searching') { - currentRideState.value = RideState.searching; - isSearchingWindow = true; - } else if (status == 'apply' || status == 'applied') { - currentRideState.value = RideState.driverApplied; - statusRide = 'Apply'; - - // الاشتراك في موقع السائق - socket.emit('subscribe_driver_location', { - 'ride_id': rideId, - 'driver_id': driverId, - }); - - // استعادة بيانات السائق - if (rideData['driver_info'] != null) { - var dInfo = rideData['driver_info']; - passengerName = dInfo['first_name']?.toString() ?? ''; - driverPhone = dInfo['phone']?.toString() ?? ''; - model = dInfo['model']?.toString() ?? ''; - licensePlate = dInfo['license_plate']?.toString() ?? ''; - } - } else if (status == 'arrived') { - currentRideState.value = RideState.driverArrived; - statusRide = 'Arrived'; - isDriverArrivePassenger = true; - } else if (status == 'begin' || status == 'started') { - currentRideState.value = RideState.inProgress; - statusRide = 'Begin'; - rideTimerBegin = true; - - // استعادة Polyline - if (rideData['polyline'] != null) { - _restorePolyline(rideData['polyline']); - } - - rideIsBeginPassengerTimer(); - } - - update(); - _startMasterTimer(); // للـ Safety Checks - } catch (e) { - Log.print('[Startup] Error: $e'); - currentRideState.value = RideState.noRide; - _startMasterTimer(); - } - } - - Future _restorePolyline(String polylineString) async { - try { - List points = - await compute(decodePolylineIsolate, polylineString); - - polylineCoordinates.clear(); - polylineCoordinates.addAll(points); - - polyLines.clear(); - polyLines = { - ...polyLines, - Polyline( - polylineId: const PolylineId('route_direct'), - points: polylineCoordinates, - color: const Color(0xFF2196F3), - width: 6, - ) - }; - - update(); - } catch (e) { - Log.print('Error restoring polyline: $e'); - } - } - -// متغير لمنع التكرار (Race Condition Guard) - bool _isAcceptanceProcessed = false; - - // ============================================================================== - // الدالة الموحدة لمعالجة القبول (من السوكيت أو FCM) - // ============================================================================== - Future processRideAcceptance( - {Map? driverData, required String source}) async { - // 1. الحماية: إذا تم المعالجة مسبقاً، تجاهل - // نستثني حالة واحدة: إذا كنا في وضع البحث (Searching) نقبل الأمر - // إذا كنا في driverApplied، نتجاهل الأمر - if (currentRideState.value != RideState.searching && - _isAcceptanceProcessed) { - Log.print("✋ Ignored Acceptance from $source. Already processed."); - return; - } - - _rideAcceptedViaSource = source; - _socketLocationUpdatesCount = 0; - - _isAcceptanceProcessed = true; // قفل الباب - Log.print("🚀 Winner: $source triggered acceptance! Processing..."); - - // 2. إيقاف جميع التايمرات القديمة فوراً - _masterTimer?.cancel(); - // stopSearchingTimer(); // إذا كان لديك تايمر للبحث - - // 3. تحديث الحالة في الواجهة - currentRideState.value = RideState.driverApplied; - statusRide = 'Apply'; - isSearchingWindow = false; - - // 4. معالجة البيانات (تعبئة المتغيرات) - if (driverData != null && driverData.isNotEmpty) { - Log.print("📥 Populating Data from $source payload..."); - _fillDriverDataLocally(driverData); - } else { - Log.print("⚠️ No Data in Payload. Fallback to API."); - await getUpdatedRideForDriverApply(rideId); - } -// أضف هذا بعد السطر الذي تستدعي فيه RideTrackingNative - await IosLiveActivityService.startRideActivity( - rideId: rideId, - driverName: driverName ?? 'السائق', - carDetails: '$make • $carColor', - etaText: stringRemainingTimeToPassenger, - progress: 0.0, - ); - // إشعارات (الأسعار، الأمان...) - _showRideStartNotifications(); - final etaText = stringRemainingTimeToPassenger; // مثال: "8 دقائق" - final carInfo = '$make • $model • $licensePlate'; - - await RideLiveNotification.showDriverOnWay( - driverName: driverName, - etaText: etaText, - carInfo: carInfo, - ); - - update(); // تحديث الواجهة لإظهار بيانات السائق - - // 5. 🔥 العمليات الجغرافية (المسار والوقت) 🔥 - - // أ) جلب موقع السائق الأولي - await getDriverCarsLocationToPassengerAfterApplied(); - _startSocketWatchdog(); - // ب) رسم المسار وحساب الوقت - if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) { - LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last; - - // نستخدم الدالة الموحدة الجديدة للحساب والرسم - await calculateDriverToPassengerRoute(driverPos, passengerLocation); - - // ج) تشغيل التايمر المحلي (للعد التنازلي فقط) - startTimerFromDriverToPassengerAfterApplied(); - } - final int timeToPassengerSeconds = - timeToPassengerFromDriverAfterApplied; // مثلاً من السيرفر - final double distanceDriverToPassengerMeters = - double.tryParse(distanceByPassenger.toString()) ?? 0.0; - - // [PiP] تفعيل PiP عند بدء الرحلة (سيدخل وضع النافذة العائمة عند خروج المستخدم) - PipService.enablePip(); - - // 6. 🔥 القرار الذكي: هل نبدأ البولينج أم نعتمد على السوكيت؟ 🔥 - if (source == "Socket" && isSocketConnected) { - // ✅ السوكيت هو من أخبرنا بالقبول — نثق به ولا نشغل البولينج - Log.print( - "🧠 Smart Mode: Socket accepted ride. Skipping polling, relying on WebSocket."); - // الـ watchdog وحده يكفي كشبكة أمان - } else { - // ⚠️ القبول من FCM أو Polling — نشغل البولينج كالمعتاد - Log.print("🔄 Fallback Mode: $source accepted ride. Starting polling."); - _startDriverLocationPollingWithTimer(); - } - } - - Timer? _watchdogTimer; - - void _startSocketWatchdog() { - _watchdogTimer?.cancel(); - Log.print("👀 Starting Socket Watchdog (Hybrid Mode)..."); - - _watchdogTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { - if (currentRideState.value != RideState.driverApplied && - currentRideState.value != RideState.driverArrived && - currentRideState.value != RideState.inProgress) { - timer.cancel(); - return; - } - - final lastTime = _lastSocketLocationTime ?? - DateTime.now().subtract(const Duration(minutes: 1)); - final difference = DateTime.now().difference(lastTime).inSeconds; - - if (difference < 15 && isSocketConnected) { - // ✅ السوكيت صحي — تأكد أن البولينج متوقف إذا كنا في وضع السوكيت - if (_locationPollingTimer != null && - _rideAcceptedViaSource == "Socket") { - Log.print("✅ Socket recovered. Stopping polling fallback."); - _stopDriverLocationPolling(); - } - } else if (difference >= 15 && difference < 30) { - // ⚠️ تأخر متوسط — جلب واحد فقط - Log.print("⚠️ Socket silent for ${difference}s. Single API Poll..."); - await getDriverCarsLocationToPassengerAfterApplied(); - } else if (difference >= 30) { - // 🔴 السوكيت ميت — تفعيل البولينج الكامل كـ fallback - if (_locationPollingTimer == null) { - Log.print( - "🔴 Socket dead for ${difference}s. Activating polling fallback!"); - _startDriverLocationPollingWithTimer(); - } else { - // البولينج يعمل بالفعل، فقط أكمل - await getDriverCarsLocationToPassengerAfterApplied(); - } - } - }); - } - - // قائمة بأنصاف الأقطار (بالأمتار) لكل مرحلة - final List _searchRadii = [ - 2400, - 3000, - 3100 - ]; // 0 ثانية، 30 ثانية، 60 ثانية - // المدة الزمنية لكل مرحلة بحث (بالثواني) - final int _searchPhaseDurationSeconds = 30; - // المهلة الإجمالية للبحث قبل عرض خيار زيادة السعر - final int _totalSearchTimeoutSeconds = 90; // 90 ثانية -// --- noRide throttling --- - int _noRideSearchCount = 0; - final int _noRideMaxTries = 3; // نفذ البحث 6 مرات فقط - final int _noRideIntervalSec = 5; // بين كل محاولة وأخرى 5 ثواني - DateTime? _noRideNextAllowed; // متى نسمح بالمحاولة التالية - bool _noRideSearchCapped = false; // وصلنا للحد وتوقفنا - // ============== new design to manage ride state ============== - // === 1. حالة الرحلة والمؤقت الرئيسي (Single Source of Truth) === - Rx currentRideState = RideState.noRide.obs; - Timer? _masterTimer; - final int _pollingIntervalSeconds = 13; // فاصل زمني موحد للاستعلام - - void _startMasterTimer() { - // نضمن أن مؤقت واحد فقط يعمل في أي وقت - _masterTimer?.cancel(); - _masterTimer = - Timer.periodic(Duration(seconds: _pollingIntervalSeconds), (_) { - _handleRideState(currentRideState.value); - }); - } - - void stopAllTimers() { - Log.print('🛑 FORCE STOP: Stopping ALL Timers and Streams 🛑'); - - // 1. إيقاف الماكينة الرئيسية - _masterTimer?.cancel(); - _masterTimer = null; - - // 2. إيقاف مؤقتات تتبع السائق - timerToPassengerFromDriverAfterApplied?.cancel(); - _timer?.cancel(); - _uiCountdownTimer?.cancel(); - - // 3. إيقاف مؤقتات الخريطة - - _animationTimers.forEach((key, timer) => timer.cancel()); - _animationTimers.clear(); - - // 4. إغلاق الستريمز - if (!_rideStatusStreamController.isClosed) - _rideStatusStreamController.close(); - if (!_beginRideStreamController.isClosed) - _beginRideStreamController.close(); - if (!timerController.isClosed) timerController.close(); - - // 5. تصفير العدادات لمنع إعادة الدخول - isTimerRunning = false; - isBeginRideFromDriverRunning = false; - _isFetchingDriverLocation = false; - - update(); - } - - final int _maxNoRideSearch = 3; // عدد المرات القصوى - final int _noRideDelaySeconds = 6; // الفاصل الزمني بين كل بحث -// - // - // !!! يرجى استبدال الدالة القديمة بالكامل بهذه الدالة الجديدة !!! - // - // - bool _isStateProcessing = false; - - Future _handleRideState(RideState state) async { - if (_isRatingScreenOpen) { - Log.print('⛔ Rating Screen is Open. Skipping Logic.'); - stopAllTimers(); // تأكيد إضافي للإيقاف - return; - } - Log.print('Handling state: $state'); - - // int effectivePollingInterval = _pollingIntervalSeconds; - - // الحصول على الفاصل الزمني من الخريطة - int effectivePollingInterval = - _pollingIntervals[state] ?? _pollingIntervalSeconds; - - switch (state) { - case RideState.noRide: - final now = DateTime.now(); - if (_noRideSearchCount >= _noRideMaxTries) { - if (!_noRideSearchCapped) { - _noRideSearchCapped = true; - Log.print('[noRide] search capped at $_noRideMaxTries attempts'); - } - break; - } - if (_noRideNextAllowed != null && now.isBefore(_noRideNextAllowed!)) { - break; - } - _noRideSearchCount++; - Log.print('_noRideSearchCount: $_noRideSearchCount'); - _noRideNextAllowed = now.add(Duration(seconds: _noRideIntervalSec)); - String currentCarType = box.read(BoxName.carType) ?? 'yet'; - getCarsLocationByPassengerAndReloadMarker(); - getNearestDriverByPassengerLocation(); - break; - - case RideState.cancelled: - Log.print('[handleRideState] Ride cancelled. Stopping polling.'); - stopAllTimers(); - // effectivePollingInterval = 3600; - break; - - case RideState.preCheckReview: - stopAllTimers(); - _checkLastRideForReview(); - break; - - case RideState.searching: - // Guard: Don't poll if ride is not registered yet - if (rideId == 'yet' || rideId.isEmpty) break; - - // 1. التحقق من حالة الطلب (هل قبله أحد؟) عبر البولينج كشبكة أمان - try { - String statusFromServer = await getRideStatus(rideId); - if (statusFromServer == 'Apply' || statusFromServer == 'Applied') { - await processRideAcceptance(source: "Polling"); - break; - } - } catch (e) { - Log.print('Error polling getRideStatus: $e'); - } - - final now = DateTime.now(); - final int elapsedSeconds = now.difference(_searchStartTime!).inSeconds; - - // انتهاء وقت البحث الكلي - if (elapsedSeconds > _totalSearchTimeoutSeconds) { - stopAllTimers(); - currentRideState.value = RideState.noRide; - isSearchingWindow = false; - update(); - _showIncreaseFeeDialog(); - break; - } - - // 2. إدارة مراحل البحث (توسيع النطاق) - // السيناريو الجديد: لا نقوم بالقصف العشوائي، نرسل بناء على المرحلة أو مرور وقت كافٍ لدخول سائقين جدد - - int targetPhase = - (elapsedSeconds / _searchPhaseDurationSeconds).floor(); - if (targetPhase >= _searchRadii.length) { - targetPhase = _searchRadii.length - 1; - } - - // هل تغيرت المرحلة (توسع النطاق)؟ أو هل مر 10 ثواني منذ آخر محاولة إرسال؟ - // هذا يمنع إرسال الإشعار في كل دورة (كل 5 ثواني) ويقلل الازعاج - bool isNewPhase = targetPhase > _currentSearchPhase; - bool timeToScanForNewDrivers = - (elapsedSeconds % 15 == 0); // كل 15 ثانية نفحص الدخول الجديد - - if (isNewPhase || timeToScanForNewDrivers || elapsedSeconds < 5) { - _currentSearchPhase = targetPhase; - int currentRadius = _searchRadii[_currentSearchPhase]; - - Log.print( - '[Search Logic] Scanning for drivers. Phase: $_currentSearchPhase, Radius: $currentRadius'); - // استدعاء دالة الإشعار الذكية - // _findAndNotifyNearestDrivers(currentRadius); - } - - // تحديث نصوص الواجهة - if (elapsedSeconds < 5) { - driversStatusForSearchWindow = 'Your order is being prepared'.tr; - } else if (elapsedSeconds < 15) { - driversStatusForSearchWindow = 'Your order sent to drivers'.tr; - } else { - driversStatusForSearchWindow = - 'The drivers are reviewing your request'.tr; - } - update(); - break; - - case RideState.driverApplied: - // effectivePollingInterval = 10; - if (!_isDriverAppliedLogicExecuted) { - Log.print('[handleRideState] Execution driverApplied logic.'); - rideAppliedFromDriver(true); - _isDriverAppliedLogicExecuted = true; - } - - if (!isSocketConnected) { - try { - String statusFromServer = await getRideStatus(rideId); - if (statusFromServer == 'Arrived') { - currentRideState.value = RideState.driverArrived; - break; - } else if (statusFromServer == 'Begin' || - statusFromServer == 'inProgress') { - processRideBegin(); - break; - } - } catch (e) { - Log.print('Error polling for Arrived/Begin status: $e'); - } - } - // 🧠 جلب الموقع فقط إذا السوكيت غير صحي - if (!_isSocketHealthy()) { - getDriverCarsLocationToPassengerAfterApplied(); - } - break; - - case RideState.driverArrived: - if (!_isDriverArrivedLogicExecuted) { - _isDriverArrivedLogicExecuted = true; - startTimerDriverWaitPassenger5Minute(); - driverArrivePassengerDialoge(); - } - // effectivePollingInterval = 8; - break; - - case RideState.inProgress: - // effectivePollingInterval = 6; - - if (!isSocketConnected) { - try { - String statusFromServer = await getRideStatus(rideId); - - // !!! هنا التغيير الجذري !!! - if (statusFromServer == 'Finished' || - statusFromServer == 'finished') { - Log.print( - '🏁 DETECTED FINISHED: Killing processes and forcing Review.'); - - // 1. قتل العمليات فوراً - stopAllTimers(); - - // 2. تغيير الحالة الداخلية لمنع أي كود آخر من العمل - currentRideState.value = RideState.preCheckReview; - - // 3. تنظيف الواجهة - tripFinishedFromDriver(); - - // 4. استدعاء شاشة التقييم فوراً - _checkLastRideForReview(); - - return; // خروج نهائي من الدالة لمنع أي كود بالأسفل من التنفيذ - } - } catch (e) { - Log.print('Error polling status: $e'); - } - } - - // بقية كود التتبع العادي (لن يتم الوصول إليه إذا انتهت الرحلة) - if (!_isRideBeginLogicExecuted) { - _isRideBeginLogicExecuted = true; - _executeBeginRideLogic(); - } - // 🧠 جلب الموقع فقط إذا السوكيت غير صحي - if (!_isSocketHealthy()) { - getDriverCarsLocationToPassengerAfterApplied(); - } - break; - case RideState.finished: - tripFinishedFromDriver(); - stopAllTimers(); - effectivePollingInterval = 3600; - break; - } - // تحديث الماكينة الرئيسية إذا تغير الفاصل الزمني - _startMasterTimerWithInterval(effectivePollingInterval); - } - - int _masterIntervalSeconds = -1; - - void _startMasterTimerWithInterval(int seconds) { - // نفس الانترفَل؟ لا تعمل شيء - if (_masterTimer != null && _masterIntervalSeconds == seconds) return; - - _masterIntervalSeconds = seconds; - _masterTimer?.cancel(); - - _masterTimer = Timer.periodic(Duration(seconds: seconds), (_) { - _handleRideState(currentRideState.value); - }); - } - - Future _checkInitialRideStatus() async { - // 1. جلب الحالة من السيرفر (باستخدام getRideStatusFromStartApp) - await getRideStatusFromStartApp(); - String _status = rideStatusFromStartApp['data']['status']; - // Log.print('rideStatusFromStartApp: ${rideStatusFromStartApp}'); - // Log.print('_status: ${_status}'); - - if (_status == 'waiting' || _status == 'Apply' || _status == 'Begin') { - // رحلة جارية - rideId = rideStatusFromStartApp['data']['rideId'].toString(); - currentRideState.value = _status == 'waiting' - ? RideState.searching - : _status == 'Apply' - ? RideState.driverApplied - : _status == 'Begin' - ? RideState.inProgress - : _status == 'Cancel' - ? RideState.cancelled - : RideState.noRide; - } else if (_status == 'Finished') { - // رحلة منتهية/ملغاة - if (rideStatusFromStartApp['data']['needsReview'] == 1) { - currentRideState.value = RideState.preCheckReview; - } else { - currentRideState.value = RideState.noRide; - } - } else { - currentRideState.value = RideState.noRide; - } - - // بدء المعالجة الفورية - _handleRideState(currentRideState.value); - } - - Future _checkLastRideForReview() async { - Log.print('⭐ FORCE OPEN RATING PAGE (Get.to mode)'); - - // جلب البيانات - await getRideStatusFromStartApp(); - - if (rideStatusFromStartApp['data'] == null) { - currentRideState.value = RideState.noRide; - _startMasterTimer(); - return; - } - - String needsReview = - rideStatusFromStartApp['data']['needsReview'].toString(); - - if (needsReview == '1') { - _isRatingScreenOpen = true; - // 1. تجهيز البيانات (Arguments) - var args = { - 'driverId': rideStatusFromStartApp['data']['driver_id'].toString(), - 'rideId': rideStatusFromStartApp['data']['rideId'].toString(), - 'driverName': rideStatusFromStartApp['data']['driverName'], - 'price': rideStatusFromStartApp['data']['price'], - }; - - // 2. استخدام Get.to مع await (هذا هو الحل الجذري) - // الكود سيتوقف هنا ولن يكمل التنفيذ حتى يتم إغلاق صفحة التقييم - await Get.to( - () => RatingDriverBottomSheet(), - arguments: args, // تمرير البيانات بالطريقة التي تريدها - preventDuplicates: true, // لمنع فتح الصفحة مرتين - popGesture: false, // لمنع السحب للرجوع (في iOS) - ); - - // 3. هذا الكود لن يتنفذ إلا بعد أن يضغط المستخدم "تم" في التقييم ويغلق الصفحة - Log.print('✅ Rating Page Closed. Resetting App.'); - _isRatingScreenOpen = false; - restCounter(); - currentRideState.value = RideState.noRide; - _startMasterTimer(); // إعادة تشغيل البحث الآن فقط - } else { - currentRideState.value = RideState.noRide; - _startMasterTimer(); - } - } - - void startSearchingForDriver() async { - // ✅ منع الضغط المزدوج - if (currentRideState.value == RideState.searching) { - return; - } - // 1. تحديث الحالة الأولية - isSearchingWindow = true; - currentRideState.value = RideState.searching; - driversStatusForSearchWindow = 'Searching for nearby drivers...'.tr; - update(); - - // 2. إرسال الطلب للسيرفر (add_ride.php) - bool rideCreated = await postRideDetailsToServer(); - - if (!rideCreated) { - // فشل الإنشاء - isSearchingWindow = false; - currentRideState.value = RideState.noRide; - mySnackbarWarning("Could not create ride. Please try again.".tr); - update(); - return; - } - - // 3. نجاح الإنشاء: إضافة لجدول الانتظار المحلي (اختياري حسب منطقك) - _addRideToWaitingTable(); - - // 4. 🔥 الاتصال بالسوكيت فوراً وانتظار الرد الحقيقي 🔥 - // نغلق أي اتصال سابق ونبدأ اتصالاً جديداً مخصصاً لهذه الرحلة - initConnectionWithSocket(); - - // تشغيل الماكينة الرئيسية للمراقبة (كحماية إضافية) - // _startMasterTimer(); - } - - /// دالة لإظهار النافذة المنبثقة لزيادة السعر - void _showIncreaseFeeDialog() { - Get.dialog( - CupertinoAlertDialog( - title: Text("No drivers accepted your request yet".tr), - content: Text( - "Increasing the fare might attract more drivers. Would you like to increase the price?" - .tr), - actions: [ - CupertinoDialogAction( - child: Text("Cancel Ride".tr, - style: TextStyle(color: AppColor.redColor)), - onPressed: () { - Get.back(); - changeCancelRidePageShow(); - // cancelRide(); // دالة إلغاء الرحلة - }, - ), - CupertinoDialogAction( - child: Text("Increase Fare".tr, - style: TextStyle(color: AppColor.greenColor)), - onPressed: () { - Get.back(); - // هنا يمكنك عرض نافذة أخرى لإدخال السعر الجديد - // وبعدها استدعاء الدالة التالية - // كمثال، سنزيد السعر بنسبة 10% - double newPrice = totalPassenger * 1.10; - increasePriceAndRestartSearch(newPrice); - }, - ), - ], - ), - barrierDismissible: false, - ); - } - - /// دالة لتحديث السعر وإعادة بدء البحث - Future increasePriceAndRestartSearch(double newPrice) async { - totalPassenger = newPrice; - update(); - - // await CRUD().post(link: AppLink.updateRides, payload: { - // "id": rideId, - // "price": newPrice.toStringAsFixed(2), - // }); - CRUD().post(link: "${AppLink.server}/ride/rides/update.php", payload: { - "id": rideId, - "price": newPrice.toStringAsFixed(2), - }); - - // تصفير القائمة لأن السعر تغير، ويجب إبلاغ الجميع (حتى من وصله الإشعار سابقاً) - Log.print( - '[increasePrice] Price changed. Clearing notified list to resend.'); - notifiedDrivers.clear(); - - _searchStartTime = DateTime.now(); - _currentSearchPhase = 0; - - isSearchingWindow = true; - update(); - - _startMasterTimer(); - } - - /// (دالة جديدة موحدة) - /// هذه هي الدالة الوحيدة المسؤولة عن بدء عملية قبول الرحلة. - /// يتم استدعاؤها إما من إشعار Firebase أو من البحث الدوري (Polling). - /// هي تحتوي على "حارس البوابة" لمنع تضارب السباق. - /// (دالة موحدة وصارمة) - /// تستدعى من FCM أو من Polling عند اكتشاف قبول السائق - /// تمنع تضارب السباق وتضمن تنفيذ المنطق مرة واحدة فقط - /// متغير لمنع التكرار (Race Condition Guard) - - Future driverArrivePassengerDialoge() { - return Get.defaultDialog( - barrierDismissible: false, - title: 'Hi ,I Arrive your site'.tr, - titleStyle: AppStyle.title, - middleText: 'Please go to Car Driver'.tr, - middleTextStyle: AppStyle.title, - confirm: MyElevatedButton( - title: 'Ok I will go now.'.tr, - onPressed: () { - NotificationService.sendNotification( - target: driverToken.toString(), - title: 'Hi ,I will go now'.tr, - body: 'I will go now'.tr, - isTopic: false, // Important: this is a token - tone: 'ding', - driverList: [], - category: 'Hi ,I will go now', - ); - - Get.back(); - remainingTime = 0; - update(); - })); - } - - /// (دالة خاصة جديدة) - /// تحتوي على كل المنطق الفعلي لبدء الرحلة. - /// - Timer? _waitPassengerTimer; - static const int _waitPassengerTotalSeconds = 300; - int _waitPassengerElapsedSeconds = 0; - - /// **إيقاف عداد انتظار الراكب (Stop Wait Timer)** - /// - /// تقوم هذه الدالة بإلغاء التايمر النشط فوراً لتحرير الموارد ومنع تسريب الذاكرة. - /// - /// * [resetUI]: (اختياري) عند وضعه `true`، يتم تصفير العدادات وتحديث الواجهة لإخفاء التوقيت القديم. - void _stopWaitPassengerTimer({bool resetUI = false}) { - // 1. الإلغاء الآمن للتايمر (Safe Cancellation) - _waitPassengerTimer?.cancel(); - _waitPassengerTimer = null; - - // 2. تصفير قيم الواجهة (Reset State) - if (resetUI) { - progressTimerDriverWaitPassenger5Minute = 0.0; - remainingTimeDriverWaitPassenger5Minute = 0; - stringRemainingTimeDriverWaitPassenger5Minute = '00:00'; - - // ✅ تحديث الواجهة فوراً (GetX) - update(); - } - } - - void _executeBeginRideLogic() { - Log.print('[executeBeginRideLogic] تنفيذ منطق بدء الرحلة...'); - _stopWaitPassengerTimer(resetUI: true); // <-- إضافة - - // 1. تصفير كل عدادات ما قبل الرحلة - timeToPassengerFromDriverAfterApplied = 0; - remainingTime = 0; - remainingTimeToPassengerFromDriverAfterApplied = 0; - remainingTimeDriverWaitPassenger5Minute = 0; - - // 2. تحديث الحالة والواجهة - rideTimerBegin = true; - statusRide = 'Begin'; - isDriverInPassengerWay = false; - isDriverArrivePassenger = false; // لإخفاء واجهة "السائق وصل" - - // 3. (من كود الإشعار الخاص بك) - box.write(BoxName.passengerWalletTotal, '0'); - update(); // تحديث الواجهة قبل بدء المؤقتات - - // 4. بدء مؤقتات الرحلة الفعلية - rideIsBeginPassengerTimer(); // مؤقت عداد مدة الرحلة - // runWhenRideIsBegin(); // مؤقت تتبع موقع السائق أثناء الرحلة - - // 5. إشعار الراكب (من كود الإشعار الخاص بك) - NotificationController().showNotification( - 'Trip is Begin'.tr, - 'The trip has started! Feel free to contact emergency numbers, share your trip, or activate voice recording for the journey' - .tr, - 'start'); - } - - // متغير لمنع التكرار - bool _isRideStartedProcessed = false; - - /// **معالجة بدء الرحلة الموحدة (Unified Ride Start Handler)** - /// - /// تستدعى عند استلام حدث بدء الرحلة سواء من السوكيت أو FCM. - /// تضمن انتقال التطبيق لحالة [RideState.inProgress] مرة واحدة فقط. - Future processRideBegin({String source = "Unknown"}) async { - // منطقك الحالي - if (currentRideState.value == RideState.inProgress || - _isRideStartedProcessed) { - return; - } - - _isRideStartedProcessed = true; - currentRideState.value = RideState.inProgress; - statusRide = 'Begin'; - - // إيقاف مؤقت الانتظار - remainingTimeDriverWaitPassenger5Minute = 0; - _stopWaitPassengerTimer(); - - // 1) بيانات السائق والرحلة - rideIsBeginPassengerTimer(); - update(); - } - - late Duration durationToAdd; - late DateTime newTime = DateTime.now(); - int hours = 0; - int minutes = 0; - - // --- إضافة جديدة: للوصول إلى وحدة التحكم بالروابط --- - final DeepLinkController _deepLinkController = - Get.isRegistered() - ? Get.find() - : Get.put(DeepLinkController()); - // ------------------------------------------------ - - void onChangedPassengerCount(int newValue) { - selectedPassengerCount = newValue; - update(); - } - - void onChangedPassengersChoose() { - isPassengerChosen = true; - update(); - } - - void getCurrentLocationFormString() async { - currentLocationToFormPlaces = true; - currentLocationString = 'Waiting for your location'.tr; - await getLocation(); - currentLocationString = passengerLocation.toString(); - newStartPointLocation = passengerLocation; - update(); - } - - List coordinatesWithoutEmpty = []; - void getMapPointsForAllMethods() async { - clearPolyline(); - isMarkersShown = false; - isWayPointStopsSheetUtilGetMap = false; - isWayPointSheet = false; - durationToRide = 0; - distanceOfDestination = 0; - wayPointSheetHeight = 0; - remainingTime = 25; - haveSteps = true; - - // Filter out empty value - coordinatesWithoutEmpty = - placesCoordinate.where((coord) => coord.isNotEmpty).toList(); - latestPosition = LatLng( - double.parse(coordinatesWithoutEmpty.last.split(',')[0]), - double.parse(coordinatesWithoutEmpty.last.split(',')[1])); - for (var i = 0; i < coordinatesWithoutEmpty.length; i++) { - if ((i + 1) < coordinatesWithoutEmpty.length) { - await getMapPoints( - coordinatesWithoutEmpty[i].toString(), - coordinatesWithoutEmpty[i + 1].toString(), - i, - ); - if (i == 0) { - startNameAddress = data[0]['start_address']; - } - if (i == coordinatesWithoutEmpty.length) { - endNameAddress = data[0]['end_address']; - } - } - } - - // isWayPointStopsSheet = false; - if (haveSteps) { - String latestWaypoint = - placesCoordinate.lastWhere((coord) => coord.isNotEmpty); - latestPosition = LatLng( - double.parse(latestWaypoint.split(',')[0]), - double.parse(latestWaypoint.split(',')[1]), - ); - } - updateCameraForDistanceAfterGetMap(); - changeWayPointStopsSheet(); - bottomSheet(); - showBottomSheet1(); - - update(); - } - - void convertHintTextPlaces(int index, var res) { - if (placeListResponseAll[index].isEmpty) { - placeListResponseAll[index] = res; - hintTextwayPointStringAll[index] = 'Search for your Start point'.tr; - update(); - } else { - hintTextwayPointStringAll[index] = res['name']; - currentLocationStringAll[index] = res['name']; - placesCoordinate[index] = - '${res['geometry']['location']['lat']},${res['geometry']['location']['lng']}'; - placeListResponseAll[index] = []; - allTextEditingPlaces[index].clear(); - // double lat = wayPoint0[index]['geometry']['location']['lat']; - // double lng = wayPoint0[index]['geometry']['location']['lng']; - // newPointLocation0 = LatLng(lat, lng); - update(); - Get.back(); - } - } - - void convertHintTextDestinationNewPlaces(int index) { - if (placesDestination.isEmpty) { - hintTextDestinationPoint = 'Search for your destination'.tr; - update(); - } else { - var res = placesDestination[index]; - hintTextDestinationPoint = res['displayName']?['text'] ?? - res['formattedAddress'] ?? - 'Unknown Place'; - double? lat = res['location']?['latitude']; - double? lng = res['location']?['longitude']; - - if (lat != null && lng != null) { - newMyLocation = LatLng(lat, lng); - // 🔥 الحل: تحريك الكاميرا فوراً للهدف حتى لا يتم مسحه عند إغلاق الكيبورد 🔥 - mapController - ?.animateCamera(CameraUpdate.newLatLngZoom(newMyLocation, 16)); - } - update(); - } - } - - void convertHintTextDestinationNewPlacesFromRecent( - List recentLocations, int index) { - hintTextDestinationPoint = recentLocations[index]['name']; - double lat = recentLocations[index]['latitude']; - double lng = recentLocations[index]['longitude']; - newMyLocation = LatLng(lat, lng); - - // 🔥 تحريك الكاميرا فوراً 🔥 - mapController?.animateCamera(CameraUpdate.newLatLngZoom(newMyLocation, 16)); - update(); - } - -// final mainBottomMenuMap = GlobalKey(); - void changeBottomSheetShown({bool? forceValue}) { - if (forceValue != null) { - isBottomSheetShown = forceValue; - } else { - isBottomSheetShown = !isBottomSheetShown; - } - - heightBottomSheetShown = isBottomSheetShown == true ? 250 : 0; - update(); - } - - void changeCashConfirmPageShown() { - isCashConfirmPageShown = !isCashConfirmPageShown; - isCashSelectedBeforeConfirmRide = true; - cashConfirmPageShown = isCashConfirmPageShown == true ? 250 : 0; - // to get or sure picker point for origin //todo - // isPickerShown = true; - // clickPointPosition(); - update(); - } - - void changePaymentMethodPageShown() { - isPaymentMethodPageShown = !isPaymentMethodPageShown; - paymentPageShown = isPaymentMethodPageShown == true ? Get.height * .6 : 0; - update(); - } - - void changeMapType() { - mapType = !mapType; - // heightButtomSheetShown = isButtomSheetShown == true ? 240 : 0; - update(); - } - - void changeMapTraffic() { - mapTrafficON = !mapTrafficON; - update(); - } - - void changeisAnotherOreder(bool val) { - isAnotherOreder = val; - update(); - } - - void changeIsWhatsAppOrder(bool val) { - isWhatsAppOrder = val; - update(); - } - - void sendSMS(String to) async { - // Get the driver's phone number. - String driverPhone = - (dataCarsLocationByPassenger['message'][carsOrder]['phone'].toString()); - - // Format the message. - String message = - 'Hi! This is ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n I am using ${box.read(AppInformation.appName)} to ride with $passengerName as the driver. $passengerName \nis driving a $model\n with license plate $licensePlate.\n I am currently located at $passengerLocation.\n If you need to reach me, please contact the driver directly at\n\n $driverPhone.'; - - // Launch the URL to send the SMS. - launchCommunication('sms', to, message); - } - - String formatSyrianPhone(String phone) { - // Remove spaces and + - phone = phone.replaceAll(' ', '').replaceAll('+', ''); - - // If starts with 00963 → remove 00 → 963 - if (phone.startsWith('00963')) { - phone = phone.replaceFirst('00963', '963'); - } - - // If starts with 0963 (common mistake) → fix it - if (phone.startsWith('0963')) { - phone = phone.replaceFirst('0963', '963'); - } - - // If starts with 963 (already correct) - if (phone.startsWith('963')) { - return phone; // nothing to do - } - - // If starts with 09 → remove leading 0 → add 963 - if (phone.startsWith('09')) { - return '963' + phone.substring(1); // 9xxxxxxxxx - } - - // If starts with 9xxxxxxxxx (no country code) - if (phone.startsWith('9') && phone.length == 9) { - return '963' + phone; - } - - // Otherwise return raw phone - return phone; - } - - void sendWhatsapp(String to) async { - // Normalize phone number before sending - String formattedPhone = formatSyrianPhone(to); - - // Message body - String message = - '${'${'Hi! This is'.tr} ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n${' I am using'.tr}'} ${AppInformation.appName}${' to ride with'.tr} $passengerName${' as the driver.'.tr} $passengerName \n${'is driving a '.tr}$model\n${' with license plate '.tr}$licensePlate.\n${' I am currently located at '.tr} https://www.google.com/maps/place/${passengerLocation.latitude},${passengerLocation.longitude}.\n${' If you need to reach me, please contact the driver directly at'.tr}\n\n $driverPhone.'; - - // Send WhatsApp message - launchCommunication('whatsapp', formattedPhone, message); - } - - void changeCancelRidePageShow() { - showCancelRideBottomSheet(); - isCancelRidePageShown = !isCancelRidePageShown; - // : cancelRide(); - update(); - } - - void getDrawerMenu() { - heightMenuBool = !heightMenuBool; - widthMapTypeAndTraffic = heightMenuBool == true ? 0 : 50; - heightMenu = heightMenuBool == true ? 80 : 0; - widthMenu = heightMenuBool == true ? 110 : 0; - update(); - } - - calcualateDistsanceInMetet(LatLng prev, current) async { - double distance2 = Geolocator.distanceBetween( - prev.latitude, - prev.longitude, - current.latitude, - current.longitude, - ); - return distance2; - } - - StreamController _timerStreamController = StreamController(); - Stream get timerStream => _timerStreamController.stream; - bool isTimerFromDriverToPassengerAfterAppliedRunning = true; - bool isTimerRunning = false; // Flag to track if the timer is running - int beginRideInterval = 10; // Interval in seconds for getBeginRideFromDriver - - void startTimerFromDriverToPassengerAfterApplied() { - stopTimerFromDriverToPassengerAfterApplied(); - if (isTimerRunning) return; - isTimerRunning = true; - isTimerFromDriverToPassengerAfterAppliedRunning = true; - - int secondsElapsed = 0; - - // استدعاء فوري لأول مرة - // getDriverCarsLocationToPassengerAfterApplied(); - - Timer.periodic(const Duration(seconds: 1), (timer) { - // --- التغيير الجوهري هنا --- - // شرط الإيقاف: نتوقف فقط إذا انتهت الرحلة أو ألغيت، أو تم إيقاف التايمر يدوياً - // لم نعد نعتمد على تجاوز الوقت المقدر (timeToPassenger) كشرط للإيقاف - bool isRideActive = (statusRide == 'Apply' || - statusRide == 'Arrived' || - statusRide == 'Begin' || - currentRideState.value == RideState.driverApplied || - currentRideState.value == RideState.driverArrived || - currentRideState.value == RideState.inProgress); - - if (!isRideActive || !isTimerFromDriverToPassengerAfterAppliedRunning) { - timer.cancel(); - isTimerRunning = false; - if (!_timerStreamController.isClosed) { - _timerStreamController.close(); - } - return; - } - - secondsElapsed++; - if (!_timerStreamController.isClosed) { - _timerStreamController.add(secondsElapsed); - } - - // تحديث الواجهة للوقت المتبقي (شكلياً فقط للراكب) - // حتى لو أصبح الوقت سالباً (تأخر السائق)، سنظهره كـ 00:00 أو نتركه سالباً - remainingTimeToPassengerFromDriverAfterApplied = - timeToPassengerFromDriverAfterApplied - secondsElapsed; - - if (remainingTimeToPassengerFromDriverAfterApplied < 0) { - remainingTimeToPassengerFromDriverAfterApplied = 0; - } - - int minutes = - (remainingTimeToPassengerFromDriverAfterApplied / 60).floor(); - int seconds = remainingTimeToPassengerFromDriverAfterApplied % 60; - stringRemainingTimeToPassenger = - '$minutes:${seconds.toString().padLeft(2, '0')}'; -// تحويل الوقت أو المسافة إلى نسبة من 0.0 إلى 1.0 - double currentProgress = 1 - - (remainingTimeToPassengerFromDriverAfterApplied / - timeToPassengerFromDriverAfterApplied); - - // 🔴 التعديل هنا: نحدث الآيفون كل 5 ثواني فقط للحفاظ على البطارية وتجنب حظر أبل - if (secondsElapsed % 5 == 0) { - double currentProgress = 1 - - (remainingTimeToPassengerFromDriverAfterApplied / - (timeToPassengerFromDriverAfterApplied == 0 - ? 1 - : timeToPassengerFromDriverAfterApplied)); - - IosLiveActivityService.updateRideActivity( - status: 'waiting', - driverName: driverName ?? 'السائق', - carDetails: - '$make • $model • $carColor', // من الأفضل إظهار اللون أيضاً - etaText: stringRemainingTimeToPassenger, - progress: currentProgress.clamp(0.0, 1.0), - ); - } - // جلب موقع السائق كل 4 ثواني (Polling) ما دامت الرحلة نشطة - if (secondsElapsed % beginRideInterval == 0) { - // 2. تحديث موقع الراكب للسائق - uploadPassengerLocation(); - } else { - update(); - } - }); - } - - // Function to stop the timer - void stopTimerFromDriverToPassengerAfterApplied() { - isTimerFromDriverToPassengerAfterAppliedRunning = false; - update(); - } - - void startTimerDriverWaitPassenger5Minute() { - // لا تبدأ إلا إذا فعلاً وصلنا - if (currentRideState.value != RideState.driverArrived) return; - - // 1) أوقف أي عداد سابق (تتبع وصول السائق) - stopTimerFromDriverToPassengerAfterApplied(); - isTimerRunning = false; - - // 2) أوقف عداد الانتظار إن كان شغال من قبل (منع تكرار) - _stopWaitPassengerTimer(); - - // 3) جهّز UI الانتظار - isDriverArrivePassenger = true; - isDriverInPassengerWay = false; - timeToPassengerFromDriverAfterApplied = 0; - - _waitPassengerElapsedSeconds = 0; - remainingTimeDriverWaitPassenger5Minute = _waitPassengerTotalSeconds; - progressTimerDriverWaitPassenger5Minute = 0; - - int m = (remainingTimeDriverWaitPassenger5Minute / 60).floor(); - int s = remainingTimeDriverWaitPassenger5Minute % 60; - stringRemainingTimeDriverWaitPassenger5Minute = - '$m:${s.toString().padLeft(2, '0')}'; - - update(); - - // 4) ابدأ Timer.periodic (يمكن إلغاؤه فوراً) - _waitPassengerTimer = Timer.periodic(const Duration(seconds: 1), (t) { - // أول ما تتحول إلى inProgress (أو أي حالة غير arrived) أوقف فوراً - if (currentRideState.value != RideState.driverArrived) { - _stopWaitPassengerTimer(resetUI: true); - // إخفاء واجهة "السائق وصل" إذا بدأت الرحلة - if (currentRideState.value == RideState.inProgress) { - isDriverArrivePassenger = false; - } - update(); - return; - } - - _waitPassengerElapsedSeconds++; - int remaining = _waitPassengerTotalSeconds - _waitPassengerElapsedSeconds; - if (remaining < 0) remaining = 0; - - remainingTimeDriverWaitPassenger5Minute = remaining; - progressTimerDriverWaitPassenger5Minute = - _waitPassengerElapsedSeconds / _waitPassengerTotalSeconds; - - int minutes = (remaining / 60).floor(); - int seconds = remaining % 60; - stringRemainingTimeDriverWaitPassenger5Minute = - '$minutes:${seconds.toString().padLeft(2, '0')}'; - - update(); - - if (remaining == 0) { - _stopWaitPassengerTimer(); - // هنا إذا بدك: طبّق غرامة انتظار / اعرض رسالة / إلخ - } - }); - } - - // Create a StreamController to manage the timer values - final timerController = StreamController(); - -// Start the timer when the ride begins - void beginRideTimer() { - // Set up the timer to run every second - Timer.periodic(const Duration(seconds: 1), (timer) { - // Update the timer value and notify listeners - timerController.add(timer.tick); - update(); - }); - } - -// Stop the timer when the ride ends - void stopRideTimer() { - timerController.close(); - update(); - } - - Timer? _rideProgressTimer; - bool _hasShownSpeedWarning = false; // متغير لحالة التنبيه - - /// **بدء مؤقت الرحلة للراكب (Passenger Ride Timer)** - /// - /// تقوم هذه الدالة بإدارة العداد الزمني للرحلة بمجرد بدئها (حالة [RideState.inProgress]). - /// - /// **المهام الرئيسية:** - /// 1. **دقة التوقيت:** تعتمد على فرق الوقت الحقيقي (`DateTime.difference`) لضمان دقة العداد حتى لو خرج المستخدم من التطبيق وعاد. - /// 2. **مراقبة السرعة:** تفحص سرعة المركبة كل ثانية، وتطلق تحذيراً [_triggerSpeedWarning] إذا تجاوزت 100 كم/س. - /// 3. **تحديث الواجهة:** تقوم بتحديث شريط التقدم والوقت المتبقي لحظياً. - /// 4. **الإيقاف التلقائي:** تتوقف تلقائياً عند انتهاء الوقت أو تغير حالة الرحلة. - void rideIsBeginPassengerTimer() { - // 1. تنظيف أي تايمر سابق - _rideProgressTimer?.cancel(); - _hasShownSpeedWarning = false; // تصفير حالة التنبيه - - // 2. تحديد وقت الوصول المتوقع بدقة - DateTime now = DateTime.now(); - DateTime expectedArrivalTime = now.add(Duration(seconds: durationToRide)); - - // تنسيق وقت الوصول للعرض - var arrivalTime = DateFormat('hh:mm a').format(expectedArrivalTime); - box.write(BoxName.arrivalTime, arrivalTime); - - Log.print("⏳ Ride Timer Started. Duration: $durationToRide sec"); - - // 3. بدء التايمر الدوري - _rideProgressTimer = - Timer.periodic(const Duration(seconds: 1), (timer) async { - // أ) شرط الإيقاف الحاسم: إذا انتهت الرحلة أو ألغيت - if (currentRideState.value != RideState.inProgress) { - timer.cancel(); - return; - } - - // ب) حساب الوقت المتبقي بناءً على الساعة الحالية (أدق من العد) - DateTime currentNow = DateTime.now(); - int remainingSeconds = - expectedArrivalTime.difference(currentNow).inSeconds; - - if (remainingSeconds < 0) remainingSeconds = 0; - - // تحديث المتغيرات - remainingTimeTimerRideBegin = remainingSeconds; - - // حساب النسبة المئوية (حماية من القسمة على صفر) - progressTimerRideBegin = - durationToRide > 0 ? 1 - (remainingSeconds / durationToRide) : 1.0; - - // ج) تنسيق الوقت للعرض - int minutes = (remainingSeconds / 60).floor(); - int seconds = remainingSeconds % 60; - stringRemainingTimeRideBegin = - '$minutes:${seconds.toString().padLeft(2, '0')}'; - - // نحول progressTimerRideBegin (0..1) إلى نسبة (0..100) - final percent = (progressTimerRideBegin * 100).clamp(0, 100).toInt(); - - // ============================================================== - // 🔔 د) تحديث الإشعارات (هنا تم حل مشكلة الإزعاج) - // ============================================================== - - // 1. تحديث الآيفون (Live Activity): يمكن تحديثه كل 5 ثواني لأنه "تحديث صامت" للشاشة فقط ولا يصدر صوتاً - if (remainingSeconds % 5 == 0 || remainingSeconds == 0) { - IosLiveActivityService.updateRideActivity( - status: 'ongoing', // ['waiting', 'ongoing'] - driverName: driverName ?? '', - carDetails: '$make • $model • $carColor', - etaText: stringRemainingTimeRideBegin, - progress: progressTimerRideBegin.clamp(0.0, 1.0), - ); - } - - // 2. تحديث إشعار الهاتف العادي (RideLiveNotification): - // نحدثه كل دقيقة (60 ثانية) بدلاً من 5 ثواني حتى لا يزعج الراكب بالرنين المستمر! - if (remainingSeconds % 60 == 0 || remainingSeconds == 0) { - await RideLiveNotification.showTripInProgress( - percentage: percent, - etaText: stringRemainingTimeRideBegin, - ); - } - // ============================================================== - - // هـ) منطق الإشعارات لمنتصف الرحلة (يصدر تنبيه مرة واحدة فقط) - if (progressTimerRideBegin >= 0.25 && - progressTimerRideBegin < 0.26 && - !_hasShownSpeedWarning) { - // يمكن إضافة منطق إشعار منتصف الرحلة هنا - } - - // و) مراقبة السرعة (Speed Check) - if (speed > 100 && !_hasShownSpeedWarning) { - _hasShownSpeedWarning = true; // ✅ قفل التنبيه حتى لا يتكرر - _triggerSpeedWarning(); - } - - // إعادة تفعيل التنبيه إذا انخفضت السرعة (إعادة ضبط الأمان) - if (speed < 80 && _hasShownSpeedWarning) { - _hasShownSpeedWarning = false; - } - - // ز) إنهاء التايمر إذا انتهى الوقت - if (remainingSeconds <= 0) { - timer.cancel(); - } - - update(); - }); - } - - /// **عرض تحذير السرعة الزائدة (Speed Warning Trigger)** - /// - /// تظهر نافذة منبثقة (Dialog) وإشعاراً محلياً لتحذير الراكب عند اكتشاف سرعة عالية (> 100 كم/س). - /// - /// **الخيارات المتاحة للمستخدم:** - /// * **مشاركة التفاصيل:** لإرسال رسالة استغاثة عبر واتساب. - /// * **أنا بخير:** لإغلاق التنبيه والاستمرار في الرحلة. - void _triggerSpeedWarning() { - NotificationController().showNotification("Warning: Speeding detected!".tr, - 'You can call or record audio of this trip'.tr, 'tone1'); - - Get.defaultDialog( - barrierDismissible: false, - title: "Warning: Speeding detected!".tr, - titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), - content: Column( - children: [ - Icon(Icons.speed, size: 50, color: AppColor.redColor), - const SizedBox(height: 10), - Text( - "We noticed the speed is exceeding 100 km/h. Please slow down for your safety..." - .tr, - textAlign: TextAlign.center, - style: AppStyle.title, - ), - ], - ), - confirm: MyElevatedButton( - title: "Share Trip Details".tr, - kolor: AppColor.redColor, - onPressed: () { - Get.back(); - _shareTripDetailsSOS(); - }, - ), - cancel: MyElevatedButton( - title: "I'm Safe".tr, - kolor: AppColor.greenColor, - onPressed: () { - Get.back(); - }, - ), - ); - } - - /// **تفعيل وضع الطوارئ للمركبة (sosPassenger)** - /// - /// تقوم بإظهار حوار تأكيدي للمستخدم لسؤاله عما إذا كان يرغب في إرسال - /// إشارة استغاثة عبر واتساب. - void sosPassenger() { - Get.defaultDialog( - barrierDismissible: false, - title: "Emergency SOS".tr, - titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), - content: Column( - children: [ - Icon(Icons.warning_amber_rounded, size: 50, color: AppColor.redColor), - const SizedBox(height: 10), - Text( - "Do you want to send an emergency message to your SOS contact?".tr, - textAlign: TextAlign.center, - style: AppStyle.title, - ), - ], - ), - confirm: MyElevatedButton( - title: "Send SOS".tr, - kolor: AppColor.redColor, - onPressed: () { - Get.back(); - _shareTripDetailsSOS(); - }, - ), - cancel: MyElevatedButton( - title: "I'm Safe".tr, - kolor: AppColor.greenColor, - onPressed: () { - Get.back(); - }, - ), - ); - } - - /// **مشاركة تفاصيل الرحلة للطوارئ (SOS Share)** - /// - /// تقوم بتجهيز رسالة نصية مفصلة تحتوي على بيانات الرحلة الحالية وإرسالها - /// عبر تطبيق WhatsApp لرقم الطوارئ المحفوظ. - /// - /// **البيانات المرسلة:** - /// * موقع الانطلاق والوصول. - /// * اسم السائق، رقم الهاتف، ونوع السيارة. - /// * رابط مباشر للموقع الحالي على خرائط جوجل. - void _shareTripDetailsSOS() { - String message = "**Emergency SOS from Passenger:**\n"; - String origin = startNameAddress; - String destination = endNameAddress; - - message += "* ${'Origin'.tr}: $origin\n"; - message += "* ${'Destination'.tr}: $destination\n"; - message += "* ${'Driver Name'.tr}: $driverName\n"; - message += "* ${'Car'.tr}: $make - $model - $licensePlate\n"; - message += "* ${'Phone'.tr}: $driverPhone\n\n"; - - // رابط جوجل مابس صحيح - message += - "${'Location'.tr}: https://www.google.com/maps/search/?api=1&query=${passengerLocation.latitude},${passengerLocation.longitude}\n"; - message += "Please help! Contact me as soon as possible.".tr; - - launchCommunication( - 'whatsapp', box.read(BoxName.sosPhonePassenger), message); - } - - int progressTimerRideBeginVip = 0; - int elapsedTimeInSeconds = 0; // Timer starts from 0 - String stringElapsedTimeRideBegin = '0:00'; - String stringElapsedTimeRideBeginVip = '0:00'; - bool rideInProgress = true; // To control when to stop the timer - - void rideIsBeginPassengerTimerVIP() async { - rideInProgress = true; // Start the ride timer - bool sendSOS = false; - while (rideInProgress) { - await Future.delayed(const Duration(seconds: 1)); - - // Increment elapsed time - elapsedTimeInSeconds++; - - // Update the time display - int minutes = (elapsedTimeInSeconds / 60).floor(); - int seconds = elapsedTimeInSeconds % 60; - stringElapsedTimeRideBeginVip = - '$minutes:${seconds.toString().padLeft(2, '0')}'; - - // Check for speed and SOS conditions - if (speed > 100 && !sendSOS) { - Get.defaultDialog( - barrierDismissible: false, - title: "Warning: Speeding detected!".tr, - titleStyle: AppStyle.title, - content: Text( - "We noticed the speed is exceeding 100 km/h. Please slow down for your safety. If you feel unsafe, you can share your trip details with a contact or call the police using the red SOS button." - .tr, - style: AppStyle.title, - ), - confirm: MyElevatedButton( - title: "Share Trip Details".tr, - onPressed: () { - Get.back(); - // Implement sharing trip details logic here - String message = "**Emergency SOS from Passenger:**\n"; - - // Get trip details from GetX or relevant provider - String origin = passengerLocation.toString(); - String destination = myDestination.toString(); - String driverName = passengerName; - String driverCarPlate = licensePlate; - - // Add trip details to the message - message += "* ${'Origin'.tr}: $origin\n"; - message += "* ${'Destination'.tr}: $destination\n"; - message += "* ${'Driver Name'.tr}: $driverName\n"; - message += "* ${'Driver Car Plate'.tr}: $driverCarPlate\n\n"; - message += "* ${'Driver Phone'.tr}: $driverPhone\n\n"; - - // Add current location - message += - "${'Current Location'.tr}:https://www.google.com/maps/place/${passengerLocation.latitude},${passengerLocation.longitude} \n"; - - // Append a call to action - message += "Please help! Contact me as soon as possible.".tr; - - // Launch WhatsApp communication - launchCommunication( - 'whatsapp', box.read(BoxName.sosPhonePassenger), message); - sendSOS = true; - }, - kolor: AppColor.redColor, - ), - cancel: MyElevatedButton( - title: "Cancel".tr, - onPressed: () { - Get.back(); - }, - kolor: AppColor.greenColor, - ), - ); - } - - // Update the UI - update(); - } - } - - Future tripFinishedFromDriver() async { - Log.print('🧹 Cleaning UI for Finish'); - - // إغلاق أي ديالوج مفتوح - if (Get.isDialogOpen == true) Get.back(); - if (Get.isBottomSheetOpen == true) Get.back(); - - statusRide = 'Finished'; - currentRideState.value = RideState.finished; // تثبيت الحالة - - // إيقاف البحث والعدادات - isSearchingWindow = false; - rideTimerBegin = false; - shouldFetch = false; - - // إيقاف التايمرات - stopAllTimers(); - resetAllMapStates(); - clearPolyline(); - clearMarkersExceptStartEnd(); - markers.clear(); - - update(); - } - - StreamController _beginRideStreamController = - StreamController.broadcast(); - Stream get beginRideStream => _beginRideStreamController.stream; - - bool isBeginRideFromDriverRunning = false; - -// Call this method to listen to the stream - void listenToBeginRideStream() { - beginRideStream.listen((status) { - Log.print("Ride status: $status"); - // Perform additional actions based on the status - }, onError: (error) { - Log.print("Error in Begin Ride Stream: $error"); - }); - } - - begiVIPTripFromPassenger() async { - timeToPassengerFromDriverAfterApplied = 0; - remainingTime = 0; - isBottomSheetShown = false; - remainingTimeToPassengerFromDriverAfterApplied = 0; - remainingTimeDriverWaitPassenger5Minute = 0; - rideTimerBegin = true; - statusRideVip = 'Begin'; - isDriverInPassengerWay = false; - isDriverArrivePassenger = false; - update(); - // isCancelRidePageShown = true; - rideIsBeginPassengerTimerVIP(); - runWhenRideIsBegin(); - } - - Map rideStatusFromStartApp = {}; - bool isStartAppHasRide = false; - getRideStatusFromStartApp() async { - try { - var res = await CRUD().get( - link: AppLink.getRideStatusFromStartApp, - payload: {'passenger_id': box.read(BoxName.passengerID)}); - // Log.print(res); - Log.print('rideStatusFromStartApp: $res'); - // Log.print('1070'); - if (res == 'failure') { - rideStatusFromStartApp = { - 'data': {'status': 'NoRide', 'needsReview': false} - }; - isStartAppHasRide = false; - Log.print( - "No rides found for the given passenger ID within the last hour."); - } else { - var decoded = jsonDecode(res); - if (decoded['status'] == 'failure') { - rideStatusFromStartApp = { - 'data': {'status': 'NoRide', 'needsReview': false} - }; - isStartAppHasRide = false; - } else { - rideStatusFromStartApp = decoded; - } - } - if (rideStatusFromStartApp['data']['status'] == 'Begin' || - rideStatusFromStartApp['data']['status'] == 'Apply' || - rideStatusFromStartApp['data']['status'] == 'Applied') { - statusRide = rideStatusFromStartApp['data']['status']; - isStartAppHasRide = true; - RideState.inProgress; - driverId = rideStatusFromStartApp['data']['driver_id']; - passengerName = rideStatusFromStartApp['data']['driverName']; - driverRate = rideStatusFromStartApp['data']['rateDriver'].toString(); - statusRideFromStart = true; - - update(); - - Map tripData = - box.read(BoxName.tripData) as Map; - final String pointsString = tripData['polyline']; - List decodedPoints = - await compute(decodePolylineIsolate, pointsString); - - // decodePolyline(response["routes"][0]["overview_polyline"]["points"]); - for (int i = 0; i < decodedPoints.length; i++) { - polylineCoordinates.add(decodedPoints[i]); - } - var polyline = Polyline( - polylineId: const PolylineId('main_route'), - points: polylineCoordinates, - width: 6, - color: const Color(0xFF2196F3), - ); - - polyLines = {...polyLines, polyline}; - timeToPassengerFromDriverAfterApplied = 0; - remainingTime = 0; - remainingTimeToPassengerFromDriverAfterApplied = 0; - remainingTimeDriverWaitPassenger5Minute = 0; - rideTimerBegin = true; - isDriverInPassengerWay = false; - isDriverArrivePassenger = false; - // update(); - // isCancelRidePageShown = true; - durationToAdd = tripData['distance_m']; - rideIsBeginPassengerTimer(); - runWhenRideIsBegin(); - update(); - } - } catch (e) { - // Handle the error or perform any necessary actions - } - } - - void driverArrivePassenger() { - timeToPassengerFromDriverAfterApplied = 0; - remainingTime = 0; - // isCancelRidePageShown = true; - update(); - rideIsBeginPassengerTimer(); - // runWhenRideIsBegin(); - } - - void cancelTimerToPassengerFromDriverAfterApplied() { - timerToPassengerFromDriverAfterApplied?.cancel(); - } - - void clearPlacesDestination() { - placesDestination = []; - hintTextDestinationPoint = 'Search for your destination'.tr; - update(); - } - - void clearPlacesStart() { - placesStart = []; - hintTextStartPoint = 'Search for your Start point'.tr; - update(); - } - - void clearPlaces(int index) { - placeListResponseAll[index] = []; - hintTextwayPointStringAll[index] = 'Search for waypoint'.tr; - update(); - } - - int selectedReason = -1; - String? cancelNote; - void selectReason0(int index, String note) { - selectedReason = index; - cancelNote = note; - update(); - } - - void getDialog(String title, String? midTitle, VoidCallback onPressed) { - final textToSpeechController = Get.find(); - Get.defaultDialog( - title: title, - titleStyle: AppStyle.title, - middleTextStyle: AppStyle.title, - content: Column( - children: [ - IconButton( - onPressed: () async { - await textToSpeechController.speakText(title ?? midTitle!); - }, - icon: const Icon(Icons.headphones)), - Text( - midTitle!, - style: AppStyle.title, - ) - ], - ), - confirm: MyElevatedButton( - title: 'Ok'.tr, - onPressed: onPressed, - kolor: AppColor.greenColor, - ), - cancel: MyElevatedButton( - title: 'Cancel', - kolor: AppColor.redColor, - onPressed: () { - Get.back(); - })); - } - - Future?> extractCoordinatesFromLinkAsync( - String link) async { - try { - // 1. معالجة روابط الخرائط المباشرة (geo: و google.navigation:) - if (link.startsWith('geo:') || link.startsWith('google.navigation:')) { - RegExp regex = RegExp(r'(-?\d+\.\d+)[,/~=](-?\d+\.\d+)'); - var match = regex.firstMatch(link); - if (match != null) { - double lat = double.parse(match.group(1)!); - double lng = double.parse(match.group(2)!); - if (lat > 40 && lat > lng) { - double temp = lat; - lat = lng; - lng = temp; - } - return {'latitude': lat, 'longitude': lng}; - } - } - - // 2. معالجة الروابط العادية (http/https) - int urlStartIndex = link.indexOf(RegExp(r'https?://')); - if (urlStartIndex == -1) return null; - String cleanLink = link.substring(urlStartIndex).trim(); - - Uri uri = Uri.parse(cleanLink); - String finalUrl = cleanLink; - - // فك التوجيه للروابط المختصرة - if (cleanLink.contains('goo.gl') || - cleanLink.contains('maps.google.com')) { - try { - var response = - await http.get(uri).timeout(const Duration(seconds: 5)); - finalUrl = response.request?.url.toString() ?? cleanLink; - } catch (e) { - Log.print('Redirect logic failed, using original: $e'); - } - } - - // الأنماط المشتركة لخرائط جوجل (تكون دائماً Lat ثم Lng) - RegExp regex = RegExp(r'(-?\d+\.\d+)[,/~](-?\d+\.\d+)'); - var match = regex.firstMatch(finalUrl); - - if (match != null) { - double lat = double.parse(match.group(1)!); - double lng = double.parse(match.group(2)!); - - // 🔥 منطق التصحيح الذاتي (Smart Swap) للمنطقة (سوريا/الأردن/مصر) - // إذا كان الرقم الأول أكبر من الرقم الثاني بشكل واضح، فهذا يعني أن الرابط مقلوب أو أننا نحتاج للتأكد - // في منطقتنا Latitude حوالي 30-35 و Longitude حوالي 36-44 - if (lat > 40 && lat > lng) { - Log.print("⚠️ Detected Swapped Coordinates in Link. Correcting..."); - double temp = lat; - lat = lng; - lng = temp; - } - - return { - 'latitude': lat, - 'longitude': lng, - }; - } - } catch (e) { - Log.print('Error parsing location link: $e'); - } - return null; - } - - double latitudeWhatsApp = 0; - double longitudeWhatsApp = 0; - void handleWhatsAppLink(String link) async { - Map? coordinates = - await extractCoordinatesFromLinkAsync(link); - - if (coordinates != null) { - latitudeWhatsApp = coordinates['latitude']!; - longitudeWhatsApp = coordinates['longitude']!; - - Log.print( - 'Extracted coordinates: Lat: $latitudeWhatsApp, Long: $longitudeWhatsApp'); - // Use these coordinates in your app as needed - } else { - Log.print('Failed to extract coordinates from the link'); - } - } - - void goToWhatappLocation() async { - if (sosFormKey.currentState!.validate()) { - // 1. استخراج الإحداثيات أولاً بشكل محلي لضمان عدم حدوث سباق بيانات (Race Condition) - Map? coordinates = - await extractCoordinatesFromLinkAsync(whatsAppLocationText.text); - - if (coordinates != null) { - latitudeWhatsApp = coordinates['latitude']!; - longitudeWhatsApp = coordinates['longitude']!; - - Log.print( - '📍 Final Coordinates for OSM: Lat: $latitudeWhatsApp, Lng: $longitudeWhatsApp'); - - changeIsWhatsAppOrder(true); - Get.back(); - - // إعداد الوجهة - myDestination = LatLng(latitudeWhatsApp, longitudeWhatsApp); - - // تحريك الكاميرا لموقع الراكب (البداية) وليس الوجهة فوراً لضمان تحميل الخريطة - if (passengerLocation != null) { - await mapController?.animateCamera(CameraUpdate.newLatLng( - LatLng(passengerLocation.latitude, passengerLocation.longitude))); - } - - changeMainBottomMenuMap(); - passengerStartLocationFromMap = true; - isPickerShown = true; - update(); - } else { - mySnackbarWarning('لم نتمكن من استخراج الموقع من الرابط'); - } - } - } - - int currentTimeSearchingCaptainWindow = 0; - late String driverPhone = ''; - late String driverRate = ''; - late String passengerName = ''; - late String carColor = ''; - late String colorHex = ''; - late String carYear = ''; - late String model = ''; - late String make = ''; - late String licensePlate = ''; - - String driverOrderStatus = 'yet'; - bool isDriversTokensSend = false; - - Set notifiedDrivers = {}; - - /// [إضافة جديدة] - /// دالة مخصصة لإضافة الرحلة إلى جدول الانتظار (waiting_ride) - - Future _addRideToWaitingTable() async { - try { - await CRUD().post(link: AppLink.addWaitingRide, payload: { - 'id': rideId.toString(), - "start_location": - '${startLocation.latitude},${startLocation.longitude}', - "end_location": '${endLocation.latitude},${endLocation.longitude}', - "date": DateTime.now().toString(), - "time": DateTime.now().toString(), - "price": totalPassenger.toStringAsFixed(2), - 'passenger_id': box.read(BoxName.passengerID).toString(), - 'status': 'waiting', // الحالة الرئيسية لجدول الانتظار - 'carType': box.read(BoxName.carType), - 'passengerRate': passengerRate.toStringAsFixed(2), - 'price_for_passenger': totalME.toStringAsFixed(2), - 'distance': distance.toStringAsFixed(1), - 'duration': duration.toStringAsFixed(1), - 'payment_method': - Get.find().isWalletChecked ? 'wallet' : 'cash', - "passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(), - }); - Log.print('[WaitingTable] Ride $rideId added to waiting_ride table.'); - } catch (e) { - Log.print('Error adding ride to waiting_ride table: $e'); - } - } - - String driversStatusForSearchWindow = ''; - - bool isDriversDataValid() { - return dataCarsLocationByPassenger != 'failure' && - dataCarsLocationByPassenger != null && - dataCarsLocationByPassenger.containsKey('message') && - dataCarsLocationByPassenger['message'] != null; - } - - void showNoDriversDialog() { - Get.dialog( - BackdropFilter( - filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), - child: CupertinoAlertDialog( - title: Text("No Car or Driver Found in your area.".tr, - style: AppStyle.title - .copyWith(fontSize: 20, fontWeight: FontWeight.bold)), - content: Text("No Car or Driver Found in your area.".tr, - style: AppStyle.title.copyWith(fontSize: 16)), - actions: [ - CupertinoDialogAction( - onPressed: () { - Get.back(); - Get.offAll(() => const MapPagePassenger()); - }, - child: - Text('OK'.tr, style: TextStyle(color: AppColor.greenColor)), - ), - ], - ), - ), - barrierDismissible: false, - ); - } - - Future postRideDetailsToServer() async { - // التأكد من وجود مسار - if (polylineCoordinates.isEmpty) return false; - - startLocation = polylineCoordinates.first; - endLocation = polylineCoordinates.last; - - // تجهيز البيانات الكاملة (Data Enrichment) لإرسالها للـ PHP - Map payload = { - // 1. البيانات الأساسية - "start_location": '${startLocation.latitude},${startLocation.longitude}', - "end_location": '${endLocation.latitude},${endLocation.longitude}', - "date": DateTime.now().toString(), - "time": DateTime.now().toString(), - "endtime": "00:00:00", // أو حسب حساباتك - "price": totalPassenger.toStringAsFixed(2), - "passenger_id": box.read(BoxName.passengerID).toString(), - "driver_id": "0", // لم يحدد بعد - "status": "waiting", - "carType": box.read(BoxName.carType), - "price_for_driver": totalPassenger.toString(), // أو المعادلة الخاصة بك - "price_for_passenger": totalME.toString(), - "distance": distance.toString(), - - // 2. بيانات الراكب (ليستخدمها PHP لبناء الـ Payload دون استعلام) - "passenger_name": box.read(BoxName.name).toString(), - "passenger_phone": box.read(BoxName.phone).toString(), - "passenger_token": box.read(BoxName.tokenFCM).toString(), - "passenger_email": box.read(BoxName.email).toString(), - "passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(), - "passenger_rating": (passengerRate ?? 5.0).toString(), - - // 3. بيانات الواجهة الإضافية - "start_name": startNameAddress, - "end_name": endNameAddress, - "duration_text": "${(durationToRide / 60).floor()}", // نص الوقت - "distance_text": "$distance", // نص المسافة - "is_wallet": Get.find().isWalletChecked.toString(), - "has_steps": Get.find().wayPoints.length > 1 - ? 'true' - : 'false', - - // نقاط التوقف (إذا وجدت) - "step0": placesCoordinate.length > 0 ? placesCoordinate[0] : "", - "step1": placesCoordinate.length > 1 ? placesCoordinate[1] : "", - "step2": placesCoordinate.length > 2 ? placesCoordinate[2] : "", - "step3": placesCoordinate.length > 3 ? placesCoordinate[3] : "", - "step4": placesCoordinate.length > 4 ? placesCoordinate[4] : "", - }; - Log.print( - '🏁 Ride Registration Detail: $startNameAddress -> $endNameAddress'); - Log.print(' 📦 Payload: $payload'); - - try { - // الاتصال بـ add_ride.php - var response = await CRUD().post( - link: "${AppLink.server}/ride/rides/add_ride.php", // تأكد من المسار - payload: payload); - - var jsonResponse = (response is String) ? jsonDecode(response) : response; - - if (jsonResponse['status'] == 'success') { - rideId = jsonResponse['message'].toString(); // حفظ ID الرحلة - Log.print("✅ Ride Created ID: $rideId"); - return true; - } else { - Log.print("❌ Ride Creation Failed: $response"); - return false; - } - } catch (e) { - Log.print("❌ Exception in postRide: $e"); - return false; - } - } - - late LatLng endLocation; - late LatLng startLocation; - - StreamController _rideStatusStreamController = - StreamController.broadcast(); - Stream get rideStatusStream => _rideStatusStreamController.stream; - - int maxAttempts = 28; - - Future rideAppliedFromDriver(bool isApplied) async { - Log.print('[rideAppliedFromDriver] 🚀 Starting logic...'); - - // 1. جلب بيانات السائق والسيارة المحدثة من السيرفر - await getUpdatedRideForDriverApply(rideId); - - // تنبيهات الأسعار حسب نوع السيارة - if (['Speed', 'Awfar Car'].contains(box.read(BoxName.carType))) { - NotificationController().showNotification('Fixed Price'.tr, - 'The captain is responsible for the route.'.tr, 'ding'); - } else if (['Comfort', 'Lady'].contains(box.read(BoxName.carType))) { - NotificationController().showNotification('Attention'.tr, - 'The price may increase if the route changes.'.tr, 'ding'); - } - - isApplied = true; - statusRide = 'Apply'; - rideConfirm = false; - isSearchingWindow = false; - _isDriverAppliedLogicExecuted = true; // ضمان عدم التكرار - - update(); // تحديث أولي - - // 2. جلب موقع السائق الأولي فوراً (Blocking await) - await getDriverCarsLocationToPassengerAfterApplied(); - - // 3. إذا توفر الموقع: حساب المسافة/الزمن ورسم المسار - if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) { - LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last; - - Log.print( - '[rideAppliedFromDriver] 📍 Driver at: $driverPos, Passenger at: $passengerLocation'); - - // أ) استدعاء API لحساب المسافة والزمن الدقيق (بدون رسم) - await getInitialDriverDistanceAndDuration(driverPos, passengerLocation); - - // ب) رسم خط المسار (Visual only) - await drawDriverPathOnly(driverPos, passengerLocation); - - // ج) ضبط الكاميرا لتشمل السائق والراكب - _fitCameraToPoints(driverPos, passengerLocation); - } else { - Log.print( - '[rideAppliedFromDriver] ⚠️ Warning: Driver location not found yet.'); - } - - // 4. تشغيل تايمر التتبع المستمر (الذي سيقوم بتناقص الوقت الذي جلبناه من API) - startTimerFromDriverToPassengerAfterApplied(); - - // إغلاق الستريم القديم - if (!_rideStatusStreamController.isClosed) - _rideStatusStreamController.close(); - } - - /// دالة لجلب المسافة والزمن بين السائق والراكب عند قبول الطلب - /// تستخدم API سريع (overview=false) - Future getInitialDriverDistanceAndDuration( - LatLng driverPos, LatLng passengerPos) async { - final String apiUrl = 'https://routec.intaleq.xyz/route'; - final String apiKey = Env.mapKeyOsm; - - final String origin = '${driverPos.latitude},${driverPos.longitude}'; - final String dest = '${passengerPos.latitude},${passengerPos.longitude}'; - - // الرابط المطلوب: steps=false&overview=false (سريع جداً للبيانات فقط) - final Uri uri = Uri.parse( - '$apiUrl?origin=$origin&destination=$dest&steps=false&overview=false'); - - try { - Log.print('[InitialCalc] Fetching distance/duration from: $uri'); - final response = await http.get(uri, headers: {'X-API-KEY': apiKey}); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - - if (data['status'] == 'ok') { - // 1. استخراج الزمن (بالثواني) - // نستخدم المعامل 1.5348 أو 1.4 حسب منطقك السابق لتقدير الوقت الواقعي - double durationSecondsRaw = (data['duration_s'] as num).toDouble(); - int finalDurationSeconds = (durationSecondsRaw * kDurationScalar) - .toInt(); // kDurationScalar = 1.5348 - - // 2. استخراج المسافة (بالأمتار) - double distanceMeters = (data['distance_m'] as num).toDouble(); - - // 3. تحديث المتغيرات في الكنترولر - timeToPassengerFromDriverAfterApplied = finalDurationSeconds; - remainingTimeToPassengerFromDriverAfterApplied = finalDurationSeconds; - - distanceByPassenger = - (distanceMeters).toStringAsFixed(0); // المسافة نصاً - - // يمكنك أيضاً تحديث durationToPassenger إذا كنت تستخدمها - durationToPassenger = finalDurationSeconds; - - Log.print( - '[InitialCalc] ✅ Success: Duration=${finalDurationSeconds}s, Distance=${distanceMeters}m'); - update(); // تحديث الواجهة لعرض الوقت الجديد فوراً - } - } else { - Log.print('[InitialCalc] ❌ API Error: ${response.statusCode}'); - } - } catch (e) { - Log.print('[InitialCalc] 💥 Exception: $e'); - } - } - -// دالة خفيفة وسريعة لرسم خط المسار فقط (بدون أسعار أو خطوات) - Future drawDriverPathOnly(LatLng driverPos, LatLng passengerPos) async { - final String apiUrl = 'https://routec.intaleq.xyz/route'; - final String apiKey = Env.mapKeyOsm; - - final String origin = '${driverPos.latitude},${driverPos.longitude}'; - final String dest = '${passengerPos.latitude},${passengerPos.longitude}'; - - // استخدام overview=full للدقة، و steps=false للسرعة - final Uri uri = Uri.parse( - '$apiUrl?origin=$origin&destination=$dest&steps=false&overview=full'); - - try { - final response = await http.get(uri, headers: {'X-API-KEY': apiKey}); - - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - - if (data['status'] == 'ok' && data['polyline'] != null) { - final String pointsString = data['polyline']; - - // فك التشفير - List decodedPoints = - await compute(decodePolylineIsolate, pointsString); - - // إزالة خط مسار السائق القديم فقط - polyLines = polyLines - .where((p) => p.polylineId.value != 'driver_route') - .toSet(); - - // إضافة الخط الجديد - polyLines = { - ...polyLines, - Polyline( - polylineId: const PolylineId('driver_route'), - points: decodedPoints, - color: const Color(0xFF333333), // لون مميز لمسار السائق - width: 5, - ) - }; - - // لا تستدعي update هنا، سيتم استدعاؤها في الدالة الأب (getDriverCars...) لتقليل عدد التحديثات - } - } - } catch (e) { - Log.print('Error drawing driver path: $e'); - } - } - - // دالة مساعدة لضبط الكاميرا - void _fitCameraToPoints(LatLng p1, LatLng p2) async { - if (mapController == null) return; - - // 1. معالجة حالة النقاط المتطابقة (تمنع الكراش في Android) - if (p1.latitude == p2.latitude && p1.longitude == p2.longitude) { - try { - mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 17)); - } catch (e) { - Log.print("Error animating to single point: $e"); - } - return; - } - - // 2. حساب الحدود - double minLat = min(p1.latitude, p2.latitude); - double maxLat = max(p1.latitude, p2.latitude); - double minLng = min(p1.longitude, p2.longitude); - double maxLng = max(p1.longitude, p2.longitude); - - // 3. تقليل الهوامش لتجنب خطأ "View size too small" - // نستخدم 50 بدلاً من 100 ليكون آمناً مع الخرائط الصغيرة - double padding = 50.0; - - try { - await mapController?.animateCamera( - CameraUpdate.newLatLngBounds( - LatLngBounds( - southwest: LatLng(minLat, minLng), - northeast: LatLng(maxLat, maxLng), - ), - left: padding, - top: padding, - right: padding, - bottom: padding, - ), - ); - } catch (e) { - Log.print("Error animating bounds (Map might be resizing): $e"); - // محاولة بديلة آمنة: تحريك الكاميرا للمنتصف فقط دون Bounds - try { - LatLng center = LatLng((minLat + maxLat) / 2, (minLng + maxLng) / 2); - mapController?.animateCamera(CameraUpdate.newLatLngZoom(center, 14)); - } catch (e) { - Log.print("Error: $e"); - } - } - } - -// Listening to the Stream - void listenToRideStatusStream() { - rideStatusStream.listen((rideStatus) { - Log.print("Ride Status: $rideStatus"); - // Handle updates based on the ride status - }, onError: (error) { - Log.print("Error in Ride Status Stream: $error"); - // Handle stream errors - }, onDone: () { - Log.print("Ride status stream closed."); - }); - } - - void start15SecondTimer(String rideId) { - Timer(const Duration(seconds: 15), () { - // delayAndFetchRideStatusForAllDriverAvailable(rideId); - }); - } - - // Replaces void startTimer() - Timer? - _uiCountdownTimer; // Add this variable to your class to manage lifecycle - - void startUiCountdown() { - // Cancel any existing timer to avoid duplicates - _uiCountdownTimer?.cancel(); - - // Reset variables - progress = 0; - remainingTime = durationTimer; - - _uiCountdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - // Logic from your loop, but non-blocking - int i = timer.tick; // current tick - - progress = i / durationTimer; - remainingTime = durationTimer - i; - - if (remainingTime <= 0) { - timer.cancel(); // Stop this specific timer - rideConfirm = false; - - // Add the duration to the tracking time logic - timeToPassengerFromDriverAfterApplied += durationToPassenger; - - // Note: We do NOT call startTimerFromDriverToPassengerAfterApplied() here - // because we already started it in rideAppliedFromDriver! - - timerEnded(); // Call your existing completion logic - } - update(); // Update the UI progress bar - }); - } - - void timerEnded() async { - runEvery30SecondsUntilConditionMet(); - isCancelRidePageShown = false; - Log.print('isCancelRidePageShown: $isCancelRidePageShown'); - update(); - } - - Future getRideStatus(String rideId) async { - final response = await CRUD().get( - link: "${AppLink.rideServerSide}/ride/rides/getRideStatus.php", - payload: {'id': rideId}); - Log.print(response); - Log.print('2176'); - return jsonDecode(response)['data']; - } - - late String driverCarModel, - driverCarMake, - driverLicensePlate, - driverName = ''; - Future getUpdatedRideForDriverApply(String rideId) async { - // حماية مبدئية: إذا كان المعرف غير صالح لا تكمل - if (rideId == 'yet' || rideId.isEmpty) return; - - try { - final res = await CRUD().get( - link: "${AppLink.server}/ride/rides/getRideOrderID.php", - payload: {'passengerID': box.read(BoxName.passengerID).toString()}); - - if (res != 'failure') { - var response = jsonDecode(res); - Log.print('getUpdatedRideForDriverApply Response: $response'); - - // [هام] التحقق من أن data عبارة عن Map وليست false أو null - // هذا يمنع الخطأ: Class 'bool' has no instance method '[]' - if (response['status'] == 'success' && - response['data'] != null && - response['data'] is Map) { - var data = response['data']; - - // استخدام ?.toString() ?? '' للحماية من القيم الفارغة (Null Safety) - driverId = data['driver_id']?.toString() ?? ''; - driverPhone = data['phone']?.toString() ?? ''; - driverCarMake = data['make']?.toString() ?? ''; - model = data['model']?.toString() ?? ''; - colorHex = data['color_hex']?.toString() ?? ''; - carColor = data['color']?.toString() ?? ''; - make = data['make']?.toString() ?? ''; - licensePlate = data['car_plate']?.toString() ?? ''; - - // دمج الاسم الأول والأخير للراكب - String firstName = data['passengerName']?.toString() ?? ''; - String lastName = data['last_name']?.toString() ?? ''; - passengerName = - lastName.isNotEmpty ? "$firstName $lastName" : firstName; - - driverName = data['driverName']?.toString() ?? ''; - - // [هام] التوكن ضروري للإشعارات - driverToken = data['token']?.toString() ?? ''; - - carYear = data['year']?.toString() ?? ''; - driverRate = data['ratingDriver']?.toString() ?? '5.0'; - - update(); // تحديث الواجهة بالبيانات الجديدة - } else { - Log.print( - "Warning: Ride data not found or invalid (data is false/null)"); - // اختياري: يمكنك هنا التعامل مع حالة عدم العثور على السائق بعد - } - } - } catch (e) { - Log.print("Error in getUpdatedRideForDriverApply: $e"); - } - } - - late LatLng currentDriverLocation; - late double headingList; - - Map _animationTimers = {}; - final int updateIntervalMs = 100; // Update every 100ms - final double minMovementThreshold = - 10; // Minimum movement in meters to trigger update - Future getCarForFirstConfirm(String carType) async { - bool foundCars = false; - int attempt = 0; - - // Set up the periodic timer - Timer? timer = Timer.periodic(const Duration(seconds: 4), (Timer t) async { - // Attempt to get car location - foundCars = await getCarsLocationByPassengerAndReloadMarker(); - Log.print('foundCars: $foundCars'); - - if (foundCars) { - // If cars are found, cancel the timer and exit the search - t.cancel(); - } else if (attempt >= 4) { - // After 4 attempts, stop the search - t.cancel(); - - if (!foundCars) { - noCarString = true; - dataCarsLocationByPassenger = 'failure'; - } - - update(); - } - - attempt++; // Increment attempt - }); - } - - void startCarLocationSearch(String carType) { - int searchInterval = 5; // Interval in seconds - Log.print('searchInterval: $searchInterval'); - int boundIncreaseStep = 2500; // Initial bounds in meters - Log.print('boundIncreaseStep: $boundIncreaseStep'); - int maxAttempts = 3; // Maximum attempts to increase bounds - int maxBoundIncreaseStep = 6000; // Maximum bounds increase step - int attempt = 0; // Current attempt - Log.print('initial attempt: $attempt'); - - Timer.periodic(Duration(seconds: searchInterval), (Timer timer) async { - Log.print('Current attempt: $attempt'); // Log current attempt - bool foundCars = false; - if (attempt >= maxAttempts) { - timer.cancel(); - if (foundCars == false) { - noCarString = true; - // dataCarsLocationByPassenger = 'failure'; - update(); - } - - // return; - } else if (reloadStartApp == true) { - Log.print('reloadStartApp: $reloadStartApp'); - foundCars = await getCarsLocationByPassengerAndReloadMarker(); - Log.print('foundCars: $foundCars'); - - if (foundCars) { - timer.cancel(); - } else { - attempt++; - Log.print( - 'Incrementing attempt to: $attempt'); // Log incremented attempt - - if (boundIncreaseStep < maxBoundIncreaseStep) { - boundIncreaseStep += 1500; // Increase bounds - if (boundIncreaseStep > maxBoundIncreaseStep) { - boundIncreaseStep = - maxBoundIncreaseStep; // Ensure it does not exceed the maximum - } - Log.print( - 'New boundIncreaseStep: $boundIncreaseStep'); // Log new bounds - } - } - } - }); - } - - String getLocationArea(double latitude, double longitude) { - LatLng passengerPoint = LatLng(latitude, longitude); - - // 1. فحص الأردن - if (isPointInPolygon(passengerPoint, CountryPolygons.jordanBoundary)) { - box.write(BoxName.countryCode, 'Jordan'); - // يمكنك تعيين AppLink.endPoint هنا إذا كان منطقك الداخلي لا يزال يعتمد عليه - box.write(BoxName.serverChosen, - AppLink.server); // مثال: اختر سيرفر سوريا للبيانات - return 'Jordan'; - } - - // 2. فحص سوريا - if (isPointInPolygon(passengerPoint, CountryPolygons.syriaBoundary)) { - box.write(BoxName.countryCode, 'Syria'); - box.write(BoxName.serverChosen, AppLink.server); - return 'Syria'; - } - - // 3. فحص مصر - if (isPointInPolygon(passengerPoint, CountryPolygons.egyptBoundary)) { - box.write(BoxName.countryCode, 'Egypt'); - box.write(BoxName.serverChosen, AppLink.server); - return 'Egypt'; - } - - // 4. الافتراضي (إذا كان خارج المناطق المخدومة) - box.write(BoxName.countryCode, 'Jordan'); - box.write(BoxName.serverChosen, AppLink.server); - return 'Unknown Location (Defaulting to Jordan)'; - } - - Future getCarsLocationByPassengerAndReloadMarker() async { - // 1. تنظيف القائمة والماركرز - carsLocationByPassenger = []; - - if (passengerLocation.latitude == 0 && passengerLocation.longitude == 0) { - return false; // لا يوجد موقع للراكب - } - - // 2. طلب بسيط ومباشر (أنا هنا، أعطني السائقين حولي) - var res = await CRUD().get( - link: AppLink.getCarsLocationByPassenger, - payload: { - 'lat': passengerLocation.latitude.toString(), - 'lng': passengerLocation.longitude.toString(), - 'radius': '5', // نصف القطر ثابت (مثلاً 5 كم) أو يمكنك جعله ديناميكياً - 'limit': '50', // أقصى عدد سيارات للعرض - }, - ); - - if (res == 'failure') { - noCarString = true; - update(); - return false; - } - - // 3. معالجة البيانات - noCarString = false; - var responseData = jsonDecode(res); - - // دعم التنسيقين (data أو message) لضمان عدم حدوث كراش - List driversList = []; - if (responseData['status'] == true && responseData['data'] != null) { - driversList = responseData['data']; - } else if (responseData['message'] != null) { - driversList = responseData['message']; // للكود القديم احتياطاً - } - - if (driversList.isEmpty) { - carsLocationByPassenger.clear(); - update(); - return false; - } - - carsLocationByPassenger.clear(); // تنظيف الماركرز القديمة - - // 4. رسم السيارات على الخريطة - for (var i = 0; i < driversList.length; i++) { - var carData = driversList[i]; - - // التحقق من الإحداثيات لضمان عدم رسم سيارة في المحيط - double lat = double.tryParse(carData['latitude'].toString()) ?? 0.0; - double lng = double.tryParse(carData['longitude'].toString()) ?? 0.0; - double heading = double.tryParse(carData['heading'].toString()) ?? 0.0; - - if (lat == 0.0 || lng == 0.0) continue; - - _updateOrCreateMarker( - carData['id'].toString(), - LatLng(lat, lng), - heading, - // الدالة هذه تقرر شكل الأيقونة بناءً على نوع السيارة القادم من السيرفر - _getIconForCar(carData), - ); - } - - update(); - return true; - } - - final List> fakeCarData = []; - - void _addFakeCarMarkers(LatLng center, int count) { - if (fakeCarData.isEmpty) { - Random random = Random(); - double radiusInKm = 2.5; // 3 km diameter, so 1.5 km radius - - for (int i = 0; i < count; i++) { - // Generate a random angle and distance within the circle - double angle = random.nextDouble() * 2 * pi; - double distance = sqrt(random.nextDouble()) * radiusInKm; - - // Convert distance to latitude and longitude offsets - double latOffset = (distance / 111.32); // 1 degree lat ≈ 111.32 km - double lonOffset = - (distance / (111.32 * cos(radians(center.latitude)))); - - // Calculate new position - double lat = center.latitude + (latOffset * cos(angle)); - double lon = center.longitude + (lonOffset * sin(angle)); - - double heading = random.nextDouble() * 360; - - fakeCarData.add({ - 'id': 'fake_$i', - 'latitude': lat, - 'longitude': lon, - 'heading': heading, - 'gender': 'Male', // Randomize gender - }); - } - } - - for (var carData in fakeCarData) { - _updateOrCreateMarker( - carData['id'].toString(), - LatLng(carData['latitude'], carData['longitude']), - carData['heading'], - _getIconForCar(carData), - ); - } - } - - String _getIconForCar(Map carData) { - if (carData['model'].toString().contains('دراجة')) { - return motoIcon; - } else if (carData['gender'] == 'Female') { - return ladyIcon; - } else { - return carIcon; - } - } - - void _updateOrCreateMarker( - String markerId, LatLng newPosition, double newHeading, String icon) { - final mId = MarkerId(markerId); - final existingMarker = markers.cast().firstWhere( - (m) => m?.markerId == mId, - orElse: () => null, - ); - - if (existingMarker == null) { - markers = { - ...markers, - Marker( - markerId: mId, - position: newPosition, - rotation: newHeading, - icon: InlqBitmap.fromStyleImage(icon), - anchor: const Offset(0.5, 0.5), - ), - }; - update(); - } else { - double distance = - _calculateDistance(existingMarker.position, newPosition); - if (distance >= minMovementThreshold) { - _smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon); - } - } - } - - double _calculateDistance(LatLng start, LatLng end) { - // Implement distance calculation (e.g., Haversine formula) - // For simplicity, this is a placeholder. Replace with actual implementation. - return 1000 * - sqrt(pow(start.latitude - end.latitude, 2) + - pow(start.longitude - end.longitude, 2)); - } - - String formatSyrianPhoneNumber(String phoneNumber) { - // Trim any whitespace from the input. - String trimmedPhone = phoneNumber.trim(); - - // If the number starts with '09', remove the leading '0' and prepend '963'. - if (trimmedPhone.startsWith('09')) { - return '963${trimmedPhone.substring(1)}'; - } - // If the number already starts with '963', return it as is to avoid duplication. - if (trimmedPhone.startsWith('963')) { - return trimmedPhone; - } - // For any other case (e.g., number starts with '9' without a '0'), - // prepend '963' to ensure the correct format. - return '963$trimmedPhone'; - } - - String generateTrackingLink(String rideId, String driverId) { - String cleanRideId = rideId.toString().trim(); - String cleanDriverId = driverId.toString().trim(); - - // الكلمة السرية للمطابقة مع السيرفر - const String secretSalt = "Intaleq_Secure_Track_2025"; - - // الدمج والتشفير - String rawString = "$cleanRideId$cleanDriverId$secretSalt"; - var bytes = utf8.encode(rawString); - var digest = md5.convert(bytes); - String token = digest.toString(); - - // الرابط المباشر لصفحة التتبع - return "https://intaleqapp.com/track/index.php?id=$cleanRideId&token=$token"; - } - - // 2. الدالة الرئيسية (تم تعديلها لإرسال واتساب بدلاً من الإشعارات) - Future shareTripWithFamily() async { - // التحقق أولاً: هل الرقم موجود؟ - String? storedPhone = box.read(BoxName.sosPhonePassenger); - - if (storedPhone == null) { - // --- (نفس المنطق القديم: فتح ديالوج لإضافة الرقم) --- - Get.defaultDialog( - title: 'Add SOS Phone'.tr, - titleStyle: AppStyle.title, - content: Form( - key: sosFormKey, - child: MyTextForm( - controller: sosPhonePassengerProfile, - label: 'insert sos phone'.tr, - hint: 'e.g. 0912345678'.tr, - type: TextInputType.phone, - ), - ), - confirm: MyElevatedButton( - title: 'Add SOS Phone'.tr, - onPressed: () async { - if (sosFormKey.currentState!.validate()) { - Get.back(); - // تنسيق الرقم - var numberPhone = - formatSyrianPhoneNumber(sosPhonePassengerProfile.text); - - // حفظ في السيرفر - await CRUD().post( - link: AppLink.updateprofile, - payload: { - 'id': box.read(BoxName.passengerID), - 'sosPhone': numberPhone, - }, - ); - - // حفظ محلياً - box.write(BoxName.sosPhonePassenger, numberPhone); - - // استدعاء الدالة مرة أخرى للمتابعة - shareTripWithFamily(); - } - })); - return; - } - - // --- (المنطق الجديد: إرسال واتساب مباشرة) --- - - // 1. التأكد من وجود بيانات للرحلة - if (rideId == 'yet' || driverId.isEmpty) { - Get.snackbar("Alert".tr, "Wait for the trip to start first".tr); - return; - } - - // 2. تنسيق الرقم - var numberPhone = formatSyrianPhoneNumber(storedPhone); - - // 3. توليد الرابط - String trackingLink = generateTrackingLink(rideId, driverId); - - // 4. تجهيز الرسالة (بالإنجليزية وجاهزة للترجمة) - // لاحظ: استخدمت المتغيرات الموجودة في الكنترولر (passengerName هنا عادة يحمل اسم السائق في الكنترولر الخاص بك حسب الكود السابق) - String message = """ -مرحباً، تابع رحلتي مباشرة على تطبيق انطلق 🚗 - -يمكنك تتبع مسار الرحلة من هنا: -$trackingLink - -السائق: $passengerName -السيارة: $model - $licensePlate -شكراً لاستخدامك انطلق! -""" - .tr; - - String messageEn = """Hello, follow my trip live on Intaleq 🚗 - -Track my ride here: -$trackingLink - -Driver: $passengerName -Car: $model - $licensePlate -Thank you for using Intaleq! -"""; - - // اختر الرسالة بناءً على اللغة المفضلة (مثال بسيط) - String userLanguage = box.read(BoxName.lang) ?? 'ar'; - message = (userLanguage == 'ar') ? message : messageEn; -// وضعنا .tr لكي تتمكن من ترجمتها للعربية في ملفات اللغة إذا أردت، أو تركها إنجليزية - - Log.print("Sending WhatsApp to: $numberPhone"); - - // 5. فتح واتساب - launchCommunication('whatsapp', numberPhone, message); - - // (اختياري) حفظ أن التتبع مفعل لتغيير حالة الأيقونة في الواجهة - box.write(BoxName.parentTripSelected, true); - update(); - } - - Future getTokenForParent() async { - // 1. التحقق أولاً: هل الرقم موجود؟ - String? storedPhone = box.read(BoxName.sosPhonePassenger); - - if (storedPhone == null) { - // --- حالة الرقم غير موجود: نفتح الديالوج فقط --- - Get.defaultDialog( - title: 'Add SOS Phone'.tr, - titleStyle: AppStyle.title, - content: Form( - key: sosFormKey, - child: MyTextForm( - controller: sosPhonePassengerProfile, - label: 'insert sos phone'.tr, - hint: 'e.g. 0912345678'.tr, - type: TextInputType.phone, - ), - ), - confirm: MyElevatedButton( - title: 'Add SOS Phone'.tr, - onPressed: () async { - if (sosFormKey.currentState!.validate()) { - // إغلاق الديالوج الحالي - Get.back(); - - // تنسيق الرقم (تأكد أن هذا التنسيق يطابق ما تم تخزينه عند تسجيل الراكب) - var numberPhone = - formatSyrianPhoneNumber(sosPhonePassengerProfile.text); - - // حفظ الرقم في السيرفر (تحديث البروفايل) - await CRUD().post( - link: AppLink.updateprofile, - payload: { - 'id': box.read(BoxName.passengerID), - 'sosPhone': numberPhone, - }, - ); - - // حفظ الرقم محلياً - box.write(BoxName.sosPhonePassenger, numberPhone); - - // استدعاء الدالة مرة أخرى - getTokenForParent(); - } - })); - return; - } - generateTrackingLink(rideId, driverId); - // --- حالة الرقم موجود: نكمل التنفيذ --- - var numberPhone = formatSyrianPhoneNumber(storedPhone); - Log.print("Searching for Parent Token with Phone: $numberPhone"); - - // استدعاء السكريبت (استخدم POST بدلاً من GET) - var res = await CRUD() - .post(link: AppLink.getTokenParent, payload: {'phone': numberPhone}); - - // التعامل مع الاستجابة - if (res is Map) { - handleResponse(res); - } else { - try { - // var jsonRes = jsonDecode(res); - handleResponse(res); - } catch (e) { - Log.print("Error parsing response: $res"); - } - } - } - - void handleResponse(Map res) { - Log.print("Handle Response: $res"); // للتأكد من دخول الدالة - - // الحالة 1: الرقم غير مسجل (Failure) - if (res['status'] == 'failure') { - // إذا كان هناك أي ديالوج تحميل مفتوح، نغلقه أولاً، لكن بحذر - if (Get.isDialogOpen ?? false) Get.back(); - - Get.defaultDialog( - title: "No user found".tr, // اختصرت العنوان ليظهر بشكل أفضل - titleStyle: AppStyle.title, - content: Column( - children: [ - Text( - "No passenger found for the given phone number".tr, - style: AppStyle.title, // غيرت الستايل ليكون أصغر قليلاً - textAlign: TextAlign.center, - ), - const SizedBox(height: 10), - Text( - "Send Intaleq app to him".tr, - style: AppStyle.title - .copyWith(color: AppColor.greenColor, fontSize: 14), - textAlign: TextAlign.center, - ) - ], - ), - confirm: MyElevatedButton( - title: 'Send Invite'.tr, - onPressed: () { - Get.back(); // إغلاق الديالوج - - var rawPhone = box.read(BoxName.sosPhonePassenger); - // تأكد أن rawPhone ليس null - if (rawPhone == null) return; - - var phone = formatSyrianPhoneNumber(rawPhone); - - // تصحيح نص الرسالة - var message = '''Dear Friend, - -🚀 I have just started an exciting trip on Intaleq! -Download the app to track my ride: - -👉 Android: https://play.google.com/store/apps/details?id=com.Intaleq.intaleq&hl=en-US -👉 iOS: https://apps.apple.com/st/app/intaleq-rider/id6748075179 - -See you there! -Intaleq Team'''; - - launchCommunication('whatsapp', phone, message); - }), - cancel: MyElevatedButton( - title: 'Cancel'.tr, - onPressed: () { - Get.back(); - })); - } - // الحالة 2: نجاح (Success) - else if (res['status'] == 'success') { - // إغلاق أي ديالوج سابق (مثل Loading) - if (Get.isDialogOpen ?? false) Get.back(); - - Get.snackbar("Success".tr, "The invitation was sent successfully".tr, - backgroundColor: AppColor.greenColor, colorText: Colors.white); - - List tokensData = res['data']; - - for (var device in tokensData) { - String tokenParent = device['token']; - - NotificationService.sendNotification( - category: "Trip Monitoring", - target: tokenParent, - title: "Trip Monitoring".tr, - body: "Click to track the trip".tr, - isTopic: false, - tone: 'tone1', - driverList: [rideId, driverId], - ); - // حفظ آخر توكن - box.write(BoxName.tokenParent, tokenParent); - } - box.write(BoxName.parentTripSelected, true); - } - } - - // Function to check if the point is inside the polygon - bool isPointInPolygon(LatLng point, List polygon) { - int intersections = 0; - for (int i = 0; i < polygon.length; i++) { - LatLng vertex1 = polygon[i]; - LatLng vertex2 = - polygon[(i + 1) % polygon.length]; // Loop back to the start - - if (_rayIntersectsSegment(point, vertex1, vertex2)) { - intersections++; - } - } - - // If the number of intersections is odd, the point is inside - return intersections % 2 != 0; - } - -// Helper function to check if a ray from the point intersects with a polygon segment - bool _rayIntersectsSegment(LatLng point, LatLng vertex1, LatLng vertex2) { - double px = point.longitude; - double py = point.latitude; - - double v1x = vertex1.longitude; - double v1y = vertex1.latitude; - double v2x = vertex2.longitude; - double v2y = vertex2.latitude; - - // Check if the point is outside the vertical bounds of the segment - if ((py < v1y && py < v2y) || (py > v1y && py > v2y)) { - return false; - } - - // Calculate the intersection of the ray and the segment - double intersectX = v1x + (py - v1y) * (v2x - v1x) / (v2y - v1y); - - // Check if the intersection is to the right of the point - return intersectX > px; - } - - bool isInUniversity = false; -// Function to check if the passenger is in any university polygon - // Function to check if the passenger is in any university polygon and return the university name - String checkPassengerLocation(LatLng passengerLocation, - List> universityPolygons, List universityNames) { - for (int i = 0; i < universityPolygons.length; i++) { - if (isPointInPolygon(passengerLocation, universityPolygons[i])) { - isInUniversity = true; - return "Passenger is in ${universityNames[i]}"; - } - } - return "Passenger is not in any university"; - } - - String passengerLocationStringUnvirsity = 'unKnown'; - void getPassengerLocationUniversity() { - // Check if the passenger is inside any of the university polygons and get the university name - passengerLocationStringUnvirsity = checkPassengerLocation( - passengerLocation, - UniversitiesPolygons.universityPolygons, - UniversitiesPolygons.universityNames, - ); - if (passengerLocationStringUnvirsity != 'unKnown') { - // Get.snackbar('you are in $passengerLocationStringUnvirsity', ""); - } - Log.print(passengerLocationStringUnvirsity); - } - - // Initialize polygons from UniversitiesPolygons - void _initializePolygons() { - List> universityPolygons = - UniversitiesPolygons.universityPolygons; - - for (int i = 0; i < universityPolygons.length; i++) { - Polygon polygon = Polygon( - polygonId: PolygonId('univ_$i'), - points: universityPolygons[i], - fillColor: Colors.blueAccent.withOpacity(0.2), - strokeColor: Colors.blueAccent, - strokeWidth: 2, - ); - polygons.add(polygon); - } - update(); - } - - LatLng driverLocationToPassenger = const LatLng(32, 35); - Future getDriverCarsLocationToPassengerAfterApplied() async { - // driverCarsLocationToPassengerAfterApplied - // 1. الشرط الأمني: تتبع فقط إذا كانت الرحلة نشطة - bool isRideActive = (statusRide == 'Apply' || - statusRide == 'Arrived' || - statusRide == 'Begin' || - currentRideState.value == RideState.driverApplied || - currentRideState.value == RideState.driverArrived || - currentRideState.value == RideState.inProgress); - - if (!isRideActive || - statusRide == 'Finished' || - statusRide == 'Cancel' || - currentRideState.value == RideState.finished || - currentRideState.value == RideState.noRide || - currentRideState.value == RideState.preCheckReview) { - return; - } - - // 2. منع التداخل (Blocking) - if (_isFetchingDriverLocation) return; - _isFetchingDriverLocation = true; - - try { - var res = await CRUD().get( - link: AppLink.getDriverCarsLocationToPassengerAfterApplied, - payload: {'driver_id': driverId}); - - if (res != 'failure') { - datadriverCarsLocationToPassengerAfterApplied = jsonDecode(res); - - if (datadriverCarsLocationToPassengerAfterApplied['message'] != null && - datadriverCarsLocationToPassengerAfterApplied['message'] - .isNotEmpty) { - var _data = - datadriverCarsLocationToPassengerAfterApplied['message'][0]; - - LatLng newDriverPos = LatLng( - double.parse(_data['latitude'].toString()), - double.parse(_data['longitude'].toString())); -// أضف هذا السطر لتقليل استهلاك الذاكرة - if (driverCarsLocationToPassengerAfterApplied.length > 10) { - driverCarsLocationToPassengerAfterApplied.removeAt(0); - } - driverLocationToPassenger = newDriverPos; - driverCarsLocationToPassengerAfterApplied.add(newDriverPos); -// 🔥 الإضافة هنا أيضاً 🔥 -// 🔥 تحديث التوقيت حتى لو جاءت من API لكي يهدأ الحارس قليلاً - _lastSocketLocationTime = DateTime.now(); - _checkAndRecalculateIfDeviated(newDriverPos); - // [تعديل هام] تنظيف آمن: لا نحذف ماركر السائق الحالي - clearMarkersExceptStartEndAndDriver(); - - // تحريك الماركر - reloadMarkerDriverCarsLocationToPassengerAfterApplied(); - } - } - update(); - } catch (e) { - Log.print('Error fetching driver location: $e'); - } finally { - _isFetchingDriverLocation = false; - } - } - - Future runEvery30SecondsUntilConditionMet() async { - // Calculate the duration of the trip in minutes. - double tripDurationInMinutes = durationToPassenger / 5; - int loopCount = tripDurationInMinutes.ceil(); - // If the trip duration is less than or equal to 50 minutes, then break the loop. - for (var i = 0; i < loopCount; i++) { - // Wait for 50 seconds. - await Future.delayed(const Duration(seconds: 5)); - if (rideTimerBegin == true || statusRide == 'Apply') { - await getDriverCarsLocationToPassengerAfterApplied(); - reloadMarkerDriverCarsLocationToPassengerAfterApplied(); - } - } - } - - Future runWhenRideIsBegin() async { - // Calculate the duration of the trip in minutes. - double tripDurationInMinutes = durationToRide / 6; - int loopCount = tripDurationInMinutes.ceil(); - // If the trip duration is less than or equal to 50 minutes, then break the loop. - clearMarkersExceptStartEnd(); - for (var i = 0; i < loopCount; i++) { - // Wait for 50 seconds. - await Future.delayed(const Duration(seconds: 4)); - // if (rideTimerBegin == true && statusRide == 'Apply') { - await getDriverCarsLocationToPassengerAfterApplied(); - // } - reloadMarkerDriverCarsLocationToPassengerAfterApplied(); - } - } - - Timer? _timer; - // final int updateIntervalMs = 100; // Update every 100ms - // final double minMovementThreshold = - // 1.0; // Minimum movement in meters to trigger update - void clearMarkersExceptStartEndAndDriver() { - markers.removeWhere((marker) { - String id = marker.markerId.value; - // لا تحذف نقطة البداية - if (id == 'start') return false; - // لا تحذف نقطة النهاية - if (id == 'end') return false; - // لا تحذف السائق الحالي - if (id == currentDriverMarkerId) return false; - - // احذف أي شيء آخر (مثل السيارات التي ظهرت وقت البحث) - return true; - }); - - // ملاحظة: لا نستدعي update() هنا لأننا سنستدعيها في نهاية الدالة الرئيسية - } - - void clearMarkersExceptStartEnd() { - markers.removeWhere((marker) { - String id = marker.markerId.value; - return id != 'start' && id != 'end'; - }); - - update(); - } - - // 1. تعريف ID ثابت للسائق طوال الرحلة - String get currentDriverMarkerId => 'driver_marker_$driverId'; - - void reloadMarkerDriverCarsLocationToPassengerAfterApplied() { - if (datadriverCarsLocationToPassengerAfterApplied == null || - datadriverCarsLocationToPassengerAfterApplied['message'] == null || - datadriverCarsLocationToPassengerAfterApplied['message'].isEmpty) { - return; - } - - var driverData = - datadriverCarsLocationToPassengerAfterApplied['message'][0]; - - // جلب الإحداثيات الجديدة - LatLng newPosition = LatLng(double.parse(driverData['latitude'].toString()), - double.parse(driverData['longitude'].toString())); - - double newHeading = - double.tryParse(driverData['heading'].toString()) ?? 0.0; - - // تحديد الأيقونة - String icon; - if (driverData['model'].toString().contains('دراجة') || - driverData['make'].toString().contains('دراجة')) { - icon = motoIcon; - } else if (driverData['gender'] == 'Female') { - icon = ladyIcon; - } else { - icon = carIcon; - } - - // 2. البحث عن الماركر الجديد وتحديثه أو إنشاء جديد - final String markerId = currentDriverMarkerId; - final mId = MarkerId(markerId); - final existingMarker = markers.cast().firstWhere( - (m) => m?.markerId == mId, - orElse: () => null, - ); - - if (existingMarker != null) { - _smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon); - } else { - markers = { - ...markers, - Marker( - markerId: mId, - position: newPosition, - rotation: newHeading, - icon: InlqBitmap.fromStyleImage(icon), - anchor: const Offset(0.5, 0.5), - ), - }; - update(); - } - } - - // التأكد من دالة التحريك السلس - void _smoothlyUpdateMarker( - Marker oldMarker, LatLng newPosition, double newHeading, String icon) { - double distance = Geolocator.distanceBetween( - oldMarker.position.latitude, - oldMarker.position.longitude, - newPosition.latitude, - newPosition.longitude); - - if (distance < 2.0) return; - - final MarkerId markerIdKey = oldMarker.markerId; - - _animationTimers[markerIdKey.value]?.cancel(); - - int ticks = 0; - const int totalSteps = 20; - const int stepDuration = 50; - - double latStep = - (newPosition.latitude - oldMarker.position.latitude) / totalSteps; - double lngStep = - (newPosition.longitude - oldMarker.position.longitude) / totalSteps; - double headingStep = (newHeading - oldMarker.rotation) / totalSteps; - - LatLng currentPos = oldMarker.position; - double currentHeading = oldMarker.rotation; - - _animationTimers[markerIdKey.value] = - Timer.periodic(const Duration(milliseconds: stepDuration), (timer) { - ticks++; - - currentPos = - LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep); - currentHeading += headingStep; - - // Update the marker in the set - final updatedMarker = oldMarker.copyWith( - position: currentPos, - rotation: currentHeading, - icon: InlqBitmap.fromStyleImage(icon), - ); - - markers = { - ...markers.where((m) => m.markerId != markerIdKey), - updatedMarker, - }; - - // Native update through controller to avoid UI rebuild - if (mapController != null) { - mapController!.animateCamera(CameraUpdate.newLatLng( - currentPos)); // Optional: Follow car if needed - // Note: IntaleqMapController doesn't expose raw symbol update yet for Marker object, - // but declarative update via GetBuilder is fast. - } - - update(); - - if (ticks >= totalSteps) { - timer.cancel(); - _animationTimers.remove(markerIdKey.value); - } - }); - } - - void _updateMarkerPosition( - LatLng newPosition, double newHeading, String icon) { - const String markerId = 'driverToPassengers'; - - final mId = MarkerId(markerId); - final existingMarker = markers.cast().firstWhere( - (m) => m?.markerId == mId, - orElse: () => null, - ); - - if (existingMarker != null) { - _smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon); - } else { - markers = { - ...markers, - Marker( - markerId: mId, - position: newPosition, - rotation: newHeading, - icon: InlqBitmap.fromStyleImage(icon), - anchor: const Offset(0.5, 0.5), - ), - }; - update(); - } - - mapController?.animateCamera(CameraUpdate.newLatLng(newPosition)); - } - - @override - void onClose() { - Log.print( - "--- MapPassengerController: Closing and cleaning up all resources. ---"); - - // 1. إلغاء المؤقتات الفردية (باستخدام ?. الآمن) - - timerToPassengerFromDriverAfterApplied?.cancel(); - _timer?.cancel(); - _masterTimer?.cancel(); // (أضف المؤقت الرئيسي) - _camThrottle?.cancel(); // (أضف مؤقت الكاميرا) - _heartbeatTimer?.cancel(); - EmergencySignalService.instance.stopListening(); - if (isSocketConnected) { - socket.emit('unsubscribe_all', - {'passenger_id': box.read(BoxName.passengerID).toString()}); - socket.disconnect(); - socket.dispose(); - } - - // 2. إلغاء جميع المؤقتات في الخريطة (للتحريكات السلسة) - _animationTimers.forEach((key, timer) { - timer.cancel(); - }); - _animationTimers.clear(); - - // 3. إغلاق متحكمات البث (StreamControllers) لمنع تسريب الذاكرة - if (!_timerStreamController.isClosed) { - _timerStreamController.close(); - } - if (!_beginRideStreamController.isClosed) { - _beginRideStreamController.close(); - } - if (!_rideStatusStreamController.isClosed) { - _rideStatusStreamController.close(); - } - if (!timerController.isClosed) { - timerController.close(); - } - - // 4. التخلص من متحكم الخريطة (ممارسة جيدة) - mapController = null; - - Log.print("--- Cleanup complete. ---"); - super.onClose(); - } - - restCounter() { - clearPlacesDestination(); - clearPolyline(); - data = []; - rideConfirm = false; - shouldFetch = false; - timeToPassengerFromDriverAfterApplied = 0; - update(); - } - -//driver behaviour - double calculateBearing(double lat1, double lon1, double lat2, double lon2) { - double deltaLon = lon2 - lon1; - double y = sin(deltaLon) * cos(lat2); - double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(deltaLon); - double bearing = atan2(y, x); - return (bearing * 180 / pi + 360) % 360; // تحويل إلى درجات - } - - void analyzeBehavior(Position currentPosition, List routePoints) { - double actualBearing = currentPosition.heading; // الاتجاه الفعلي من GPS - double expectedBearing = calculateBearing( - routePoints[0].latitude, - routePoints[0].longitude, - routePoints[1].latitude, - routePoints[1].longitude, - ); - - double bearingDifference = (expectedBearing - actualBearing).abs(); - if (bearingDifference > 30) { - Log.print("⚠️ السائق انحرف عن المسار!"); - } - } - - void detectStops(Position currentPosition) { - if (currentPosition.speed < 0.5) { - Log.print("🚦 السائق توقف في موقع غير متوقع!"); - } - } - - Future cancelRideAfterRejectFromAll() async { - clearPlacesDestination(); - clearPolyline(); - data = []; - await CRUD().post( - link: "${AppLink.server}/ride/rides/cancel_ride_by_passenger.php", - payload: { - "ride_id": rideId.toString(), // Convert to String - "reason": 'notApplyFromAnyDriver' - }); - - rideConfirm = false; - statusRide == 'Cancel'; - isSearchingWindow = false; - shouldFetch = false; - isPassengerChosen = false; - isCashConfirmPageShown = false; - // totalStepDurations = 0; - isCashSelectedBeforeConfirmRide = false; - timeToPassengerFromDriverAfterApplied = 0; - changeCancelRidePageShow(); - remainingTime = 0; - - update(); - } - -// متغيرات أسباب الإلغاء - int selectedReasonIndex = -1; - String selectedReasonText = ""; - TextEditingController otherReasonController = TextEditingController(); - - /// تحديث السبب المختار - void selectReason(int index, String reason) { - selectedReasonIndex = index; - selectedReasonText = reason; - update(); - } - - /// **دالة إلغاء الرحلة (النهائية)** - Future cancelRide() async { - // 1. التحقق من اختيار سبب - if (selectedReasonIndex == -1) { - Get.snackbar( - 'Attention'.tr, - 'Please select a reason first'.tr, - snackPosition: SnackPosition.BOTTOM, - backgroundColor: Colors.orange, - colorText: Colors.white, - ); - return; - } - - // 2. تجهيز نص السبب النهائي - String finalReason = selectedReasonText; - if (finalReason == "Other".tr) { - if (otherReasonController.text.trim().isEmpty) { - Get.snackbar("Attention".tr, "Please write the reason...".tr, - backgroundColor: Colors.red, colorText: Colors.white); - return; - } - finalReason = otherReasonController.text.trim(); - } - - // 3. التنظيف المحلي الفوري (UX Optimization) - Get.back(); // إغلاق الـ BottomSheet - if (isCancelRidePageShown) - changeCancelRidePageShow(); // إخفاء زر الإلغاء إن وجد - - // 🔥 استدعاء دالة التنظيف الشاملة هنا 🔥 - resetAllMapStates(); - - // إيقاف جميع التايمرات - // إيقاف جميع التايمرات - stopAllTimers(); - currentRideState.value = RideState.cancelled; - await RideLiveNotification.cancel(); // إغلاق أندرويد - IosLiveActivityService.endRideActivity(); // ✅ إغلاق iOS - PipService.disablePip(); // ✅ إيقاف PiP عند الإلغاء - - // 4. الاتصال بالسيرفر لإلغاء الرحلة وإبلاغ السائق - if (rideId != 'yet' && rideId != null) { - Log.print( - '📡 Sending Cancel Request to Server with Reason: $finalReason'); - - try { - await CRUD().post( - link: "${AppLink.server}/ride/rides/cancel_ride_by_passenger.php", - payload: { - "ride_id": rideId.toString(), - "reason": finalReason // ✅ إرسال السبب للسيرفر - }, - ); - // لا داعي لإرسال FCM أو Socket يدوياً من هنا، PHP يقوم بذلك - } catch (e) { - Log.print("Error cancelling on server: $e"); - } - } - - // 5. العودة للصفحة الرئيسية - Get.offAll(() => const MapPagePassenger()); - } - - void changePickerShown() { - isPickerShown = !isPickerShown; - heightPickerContainer = isPickerShown == true ? 150 : 90; - update(); - } - - // ── Multi-Waypoint Methods ────────────────────────────────────────────────── - void addMenuWaypoint() { - if (activeMenuWaypointCount >= 2) return; - activeMenuWaypointCount++; - // Increase expanded bottom menu height to accommodate new waypoint row - mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56); - update(); - } - - void removeMenuWaypoint(int index) { - if (index < 0 || index >= 2) return; - // Shift items if removing first waypoint while second exists - if (index == 0 && activeMenuWaypointCount == 2) { - menuWaypoints[0] = menuWaypoints[1]; - menuWaypointNames[0] = menuWaypointNames[1]; - } - menuWaypoints[activeMenuWaypointCount - 1] = null; - menuWaypointNames[activeMenuWaypointCount - 1] = ''; - activeMenuWaypointCount--; - mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56); - update(); - } - - void clearAllMenuWaypoints() { - menuWaypoints = [null, null]; - menuWaypointNames = ['', '']; - activeMenuWaypointCount = 0; - isPickingWaypoint = false; - pickingWaypointIndex = -1; - update(); - } - - void startPickingWaypointOnMap(int index) { - pickingWaypointIndex = index; - isPickingWaypoint = true; - isPickerShown = true; - heightPickerContainer = 150; - // Close the expanded menu to show the map picker - isMainBottomMenuMap = true; - mainBottomMenuMapHeight = Get.height * .22; - update(); - } - - void setMenuWaypointFromMap(int index, LatLng position) { - Log.print('📍 setMenuWaypointFromMap called: index=$index, pos=$position'); - if (index < 0 || index >= 2) return; - menuWaypoints[index] = position; - menuWaypointNames[index] = - '${position.latitude.toStringAsFixed(4)}, ${position.longitude.toStringAsFixed(4)}'; - isPickingWaypoint = false; - pickingWaypointIndex = -1; - isPickerShown = false; - // Re-open expanded menu - isMainBottomMenuMap = false; - mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56); - update(); - } - - void setMenuWaypointFromSearch(int index, LatLng pos, String name) { - if (index < 0 || index >= 2) return; - menuWaypoints[index] = pos; - menuWaypointNames[index] = name; - update(); - } - - /// Build OSRM waypoint coordinate string for the route URL - String _buildOsrmWaypointCoords() { - String coords = ''; - for (int i = 0; i < activeMenuWaypointCount; i++) { - final wp = menuWaypoints[i]; - if (wp != null) { - coords += ';${wp.longitude},${wp.latitude}'; - } - } - return coords; - } - - void changeHeightPointsPageForRider() { - isPointsPageForRider = !isPointsPageForRider; - heightPointsPageForRider = isPointsPageForRider == true ? Get.height : 0; - update(); - } - - getCoordinateFromMapWayPoints(int index) { - placesCoordinate[index] = newStartPointLocation.toString(); - update(); - } - // --- ابدأ الإضافة هنا --- - -// 1. قائمة لتخزين نقاط التوقف - List> waypoints = []; - -// 2. دالة لإضافة نقطة توقف جديدة - void addWaypoint(Map placeDetails) { - // يمكنك إضافة منطق للتحقق من عدد نقاط التوقف المسموح بها هنا - waypoints.add(placeDetails); - update(); // لتحديث الواجهة - // TODO: أضف هنا استدعاء دالة إعادة رسم المسار مع نقاط التوقف الجديدة - // getDirectionMapWithWaypoints(); - } - -// 3. دالة لحذف نقطة توقف - void removeWaypoint(int index) { - if (index >= 0 && index < waypoints.length) { - waypoints.removeAt(index); - update(); // لتحديث الواجهة - // TODO: أضف هنا استدعاء دالة إعادة رسم المسار بعد حذف النقطة - // getDirectionMapWithWaypoints(); - } - } - -// --- انتهى --- - - void changeMainBottomMenuMap() { - if (isWayPointStopsSheetUtilGetMap == true) { - changeWayPointSheet(); - } else { - isMainBottomMenuMap = !isMainBottomMenuMap; - mainBottomMenuMapHeight = - isMainBottomMenuMap == true ? Get.height * .22 : Get.height * .6; - isWayPointSheet = false; - if (heightMenuBool == true) { - getDrawerMenu(); - } - initilizeGetStorage(); - update(); - } - } - - void downPoints() { - if (Get.find().wayPoints.length < 2) { - isWayPointStopsSheetUtilGetMap = false; - isWayPointSheet = false; - wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0; - // changeWayPointStopsSheet(); - update(); - } - // changeWayPointStopsSheet(); - // isWayPointSheet = false; - update(); - } - - void changeWayPointSheet() { - isWayPointSheet = !isWayPointSheet; - wayPointSheetHeight = isWayPointSheet == false ? 0 : Get.height * .45; - // if (heightMenuBool == true) { - // getDrawerMenu(); - // } - update(); - } - - void changeWayPointStopsSheet() { - // int waypointsLength = Get.find().wayPoints.length; - - if (wayPointIndex > -1) { - isWayPointStopsSheet = true; - isWayPointStopsSheetUtilGetMap = true; - } - isWayPointStopsSheet = !isWayPointStopsSheet; - wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0; - // if (heightMenuBool == true) { - // getDrawerMenu(); - // } - update(); - } - - changeHeightPlaces() { - if (placesDestination.isEmpty) { - height = 0; - update(); - } - height = 150; - update(); - } - - changeHeightStartPlaces() { - if (placesStart.isEmpty) { - height = 0; - update(); - } - height = 150; - update(); - } - - changeHeightPlacesAll(int index) { - if (placeListResponseAll[index].isEmpty) { - height = 0; - update(); - } - height = 150; - update(); - } - - changeHeightPlaces1() { - if (wayPoint1.isEmpty) { - height = 0; - update(); - } - height = 150; - update(); - } - - changeHeightPlaces2() { - if (wayPoint2.isEmpty) { - height = 0; - update(); - } - height = 150; - update(); - } - - changeHeightPlaces3() { - if (wayPoint3.isEmpty) { - height = 0; - update(); - } - height = 150; - update(); - } - - changeHeightPlaces4() { - if (wayPoint4.isEmpty) { - height = 0; - update(); - } - height = 150; - update(); - } - - hidePlaces() { - height = 0; - - update(); - } - - /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض - - // double _haversineKm(double lat1, double lon1, double lat2, double lon2) { - // const R = 6371.0; // km - // final dLat = (lat2 - lat1) * math.pi / 180.0; - // final dLon = (lon2 - lon1) * math.pi / 180.0; - // final a = math.sin(dLat / 2) * math.sin(dLat / 2) + - // math.cos(lat1 * math.pi / 180.0) * - // math.cos(lat2 * math.pi / 180.0) * - // math.sin(dLon / 2) * - // math.sin(dLon / 2); - // final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); - // return R * c; - // } - - /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض - // double _kmToLatDelta(double km) => km / 111.0; - - // /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض) - // double _kmToLngDelta(double km, double atLat) => - // km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9); - - /// حساب درجة التطابق النصي (كل كلمة تبدأ بها الاسم = 2 نقاط، يحتويها = 1 نقطة) - // double _relevanceScore(String name, String query) { - // final n = name.toLowerCase(); - // final parts = - // query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2); - // double s = 0.0; - // for (final p in parts) { - // if (n.startsWith(p)) { - // s += 2.0; - // } else if (n.contains(p)) { - // s += 1.0; - // } - // } - // return s; - // } -// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها -// انسخ هذه الدوال والصقها داخل كلاس الكنترولر الخاص بك - -// ----------------------------------------------------------------- -// --== الدالة الرئيسية للبحث ==-- -// ----------------------------------------------------------------- - /// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها -// انسخ هذه الدوال والصقها داخل كلاس الكنترولر الخاص بك - -// ----------------------------------------------------------------- -// --== الدالة الرئيسية للبحث ==-- -// ----------------------------------------------------------------- - /// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها - Future getPlaces() async { - final q = placeDestinationController.text.trim(); - if (q.isEmpty || q.length < 3) { - placesDestination = []; - update(); - return; - } - - final lat = passengerLocation.latitude; - final lng = passengerLocation.longitude; - final country = CountryPolygons.getCountryName(passengerLocation); - - try { - final url = - '${AppLink.searchGeocoding}?q=${Uri.encodeComponent(q)}&lat=$lat&lng=$lng&radius=15000&country=$country'; - final response = await CRUD().getMapSaas(link: url); - - if (response != null && response['results'] is List) { - List results = List.from(response['results']); - final List filteredResults = []; - final Set seenPlaces = {}; - - for (final p in results) { - final name = p['name_ar'] ?? p['name'] ?? ''; - final district = p['district'] ?? ''; - final plat = p['latitude']?.toString() ?? '0'; - final plng = p['longitude']?.toString() ?? '0'; - - final dedupeKey = - "${name.trim().toLowerCase()}_${district.trim().toLowerCase()}"; - - if (!seenPlaces.contains(dedupeKey)) { - seenPlaces.add(dedupeKey); - - p['distanceKm'] = (p['distance'] as num).toDouble() / 1000.0; - p['latitude'] = plat; - p['longitude'] = plng; - p['name'] = name; - p['address'] = p['full_address'] ?? - (district.isNotEmpty - ? "$district، ${p['governorate'] ?? ''}" - : (p['governorate'] ?? '')); - - filteredResults.add(p); - } - } - - placesDestination = filteredResults; - update(); - } - } catch (e) { - Log.print('Exception in getPlaces: $e'); - } - } - -// ----------------------------------------------------------------- -// --== دوال مساعدة ==-- -// ----------------------------------------------------------------- - - /// تحسب المسافة بين نقطتين بالكيلومتر (معادلة هافرساين) - double _haversineKm(double lat1, double lon1, double lat2, double lon2) { - const R = 6371.0; // نصف قطر الأرض بالكيلومتر - final dLat = (lat2 - lat1) * (pi / 180.0); - final dLon = (lon2 - lon1) * (pi / 180.0); - final rLat1 = lat1 * (pi / 180.0); - final rLat2 = lat2 * (pi / 180.0); - - final a = sin(dLat / 2) * sin(dLat / 2) + - cos(rLat1) * cos(rLat2) * sin(dLon / 2) * sin(dLon / 2); - final c = 2 * atan2(sqrt(a), sqrt(1 - a)); - return R * c; - } - - /// تحسب درجة تطابق بسيطة بين اسم المكان وكلمة البحث - double _relevanceScore(String placeName, String query) { - if (placeName.isEmpty || query.isEmpty) return 0.0; - final pLower = placeName.toLowerCase(); - final qLower = query.toLowerCase(); - if (pLower.startsWith(qLower)) return 1.0; // تطابق كامل في البداية - if (pLower.contains(qLower)) return 0.5; // تحتوي على الكلمة - return 0.0; - } - - /// تحويل كيلومتر إلى فرق درجات لخط العرض - double _kmToLatDelta(double km) { - const kmInDegree = 111.32; - return km / kmInDegree; - } - - /// تحويل كيلومتر إلى فرق درجات لخط الطول (يعتمد على خط العرض الحالي) - double _kmToLngDelta(double km, double latitude) { - const kmInDegree = 111.32; - return km / (kmInDegree * cos(latitude * (pi / 180.0))); - } - - // var languageCode; - - // // تحديد اللغة حسب الإدخال - // if (RegExp(r'[a-zA-Z]').hasMatch(placeDestinationController.text)) { - // languageCode = 'en'; - // } else { - // languageCode = 'ar'; - // } - - // final bool isTextEmpty = placeDestinationController.text.trim().isEmpty; - // var key = Platform.isAndroid ? AK.mapAPIKEY : AK.mapAPIKEYIOS; - // final Uri url = Uri.parse( - // isTextEmpty - // ? 'https://places.googleapis.com/v1/places:searchNearby?key=$key' - // : 'https://places.googleapis.com/v1/places:searchText?key=$key', - // ); - // Log.print('url: $url'); - // // بناء الجسم حسب نوع الطلب - // final body = isTextEmpty - // ? jsonEncode({ - // "languageCode": languageCode, - // "locationRestriction": { - // "circle": { - // "center": { - // "latitude": passengerLocation.latitude, - // "longitude": passengerLocation.longitude - // }, - // "radius": 40000 // 40 كم - // } - // }, - // "maxResultCount": 10 - // }) - // : jsonEncode({ - // "textQuery": placeDestinationController.text, - // "languageCode": languageCode, - // "maxResultCount": 10, - // "locationBias": { - // "circle": { - // "center": { - // "latitude": passengerLocation.latitude, - // "longitude": passengerLocation.longitude - // }, - // "radius": 40000 - // } - // } - // }); - - // final headers = { - // 'Content-Type': 'application/json', - // 'X-Goog-Api-Key': AK.mapAPIKEY, - // 'X-Goog-FieldMask': - // 'places.displayName,places.formattedAddress,places.location' - // }; - - // try { - // final response = await http.post(url, headers: headers, body: body); - // Log.print('response: ${response.statusCode} - ${response.body}'); - - // if (response.statusCode == 200) { - // final data = jsonDecode(response.body); - // placesDestination = data['places'] ?? []; - // update(); - // } else { - // Log.print('Error: ${response.statusCode} - ${response.reasonPhrase}'); - // } - // } catch (e) { - // Log.print('Exception: $e'); - // } - // } - - getAIKey(String key) async { - var res = - await CRUD().get(link: AppLink.getapiKey, payload: {"keyName": key}); - if (res != 'failure') { - var d = jsonDecode(res)['message']; - return d[key].toString(); - } else {} - } - - Future getPlacesStart() async { - final q = placeStartController.text.trim(); - if (q.isEmpty || q.length < 3) { - placesStart = []; - update(); - return; - } - - final lat = passengerLocation.latitude; - final lng = passengerLocation.longitude; - final country = CountryPolygons.getCountryName(passengerLocation); - - try { - final url = - '${AppLink.searchGeocoding}?q=${Uri.encodeComponent(q)}&lat=$lat&lng=$lng&radius=15000&country=$country'; - final response = await CRUD().getMapSaas(link: url); - - if (response != null && response['results'] is List) { - List list = List.from(response['results']); - for (final p in list) { - p['distanceKm'] = (p['distance'] as num).toDouble() / 1000.0; - p['latitude'] = p['latitude'].toString(); - p['longitude'] = p['longitude'].toString(); - p['name'] = p['name_ar'] ?? p['name'] ?? ''; - p['address'] = p['full_address'] ?? - (p['district'] != null - ? "${p['district']}، ${p['governorate'] ?? ''}" - : (p['governorate'] ?? '')); - } - placesStart = list; - update(); - } - } catch (e) { - Log.print('Exception in getPlacesStart: $e'); - } - } - - Future getPlacesListsWayPoint(int index) async { - final q = wayPoint0Controller.text.trim(); - if (q.length < 3) return; - - final lat = passengerLocation.latitude; - final lng = passengerLocation.longitude; - final country = CountryPolygons.getCountryName(passengerLocation); - - try { - final url = - '${AppLink.searchGeocoding}?q=${Uri.encodeComponent(q)}&lat=$lat&lng=$lng&radius=15000&country=$country'; - final response = await CRUD().getMapSaas(link: url); - - if (response != null && response['results'] is List) { - List list = List.from(response['results']); - for (final p in list) { - p['distanceKm'] = (p['distance'] as num).toDouble() / 1000.0; - p['latitude'] = p['latitude'].toString(); - p['longitude'] = p['longitude'].toString(); - p['name'] = p['name_ar'] ?? p['name'] ?? ''; - p['address'] = p['full_address'] ?? - (p['district'] != null - ? "${p['district']}، ${p['governorate'] ?? ''}" - : (p['governorate'] ?? '')); - } - wayPoint0 = list; - placeListResponseAll[index] = list; - update(); - } - } catch (e) { - Log.print('Error fetching places in WayPoint: $e'); - } - } - -// داخل MapPassengerController - bool lowPerf = false; - Timer? _camThrottle; - DateTime _lastUiUpdate = DateTime.fromMillisecondsSinceEpoch(0); - - Future detectPerfMode() async { - try { - if (GetPlatform.isAndroid) { - final info = await DeviceInfoPlugin().androidInfo; - final sdk = info.version.sdkInt ?? 0; - final ram = info.availableRamSize ?? 0; - lowPerf = (sdk < 28) || (ram > 0 && ram < 3 * 1024 * 1024 * 1024); - } else { - lowPerf = false; - } - } catch (_) { - lowPerf = false; - } - update(); - } - -// تحديث الكاميرا بثروتل - void onCameraMoveThrottled(CameraPosition pos) { - _camThrottle?.cancel(); - _camThrottle = Timer(const Duration(milliseconds: 160), () { - Log.print('📸 onCameraMoveThrottled: ${pos.target}'); - // ضع فقط المنطق الضروري هنا لتقليل الحمل - int waypointsLength = Get.find().wayPoints.length; - int index = wayPointIndex; - if (waypointsLength > 0) { - placesCoordinate[index] = - '${pos.target.latitude},${pos.target.longitude}'; - } - newMyLocation = pos.target; - }); - } - -// Removed legacy light polylines since MapLibre vectors handle high-point geometries natively. - - Future savePlaceToServer( - String latitude, String longitude, String name, String rate) async { - var data = { - 'latitude': latitude, - 'longitude': longitude, - 'name': name, - 'rate': rate, - }; - - try { - CRUD().post( - link: AppLink.savePlacesServer, - payload: data, - ); - } catch (e) { - Log.print('Error: $e'); - } - } - - Future getLocation() async { - Log.print('🛰️ getLocation() called'); - // Check if the app has permission to access location - permissionGranted = await location.hasPermission(); - if (permissionGranted == PermissionStatus.denied) { - permissionGranted = await location.requestPermission(); - if (permissionGranted != PermissionStatus.granted) { - // Location permission is still not granted, handle the error - return; - } - } - - // Configure location accuracy - // LocationAccuracy desiredAccuracy = LocationAccuracy.high; - - // Get the current location with a timeout to prevent hanging UI - LocationData? _locationData; - try { - _locationData = await location.getLocation().timeout( - const Duration(seconds: 5), - onTimeout: () { - Log.print("⚠️ Location fetch timed out after 5s."); - return LocationData.fromMap({ - "latitude": passengerLocation.latitude, - "longitude": passengerLocation.longitude, - "speed": 0.0 - }); - }, - ); - } catch (e) { - Log.print("⚠️ Error fetching location: $e"); - } - - if (_locationData == null) { - isLoading = false; - update(); - return; - } - passengerLocation = - (_locationData.latitude != null && _locationData.longitude != null - ? LatLng(_locationData.latitude!, _locationData.longitude!) - : null)!; - // getLocationArea(passengerLocation.latitude, passengerLocation.longitude); - // Log.print('AppLink.endPoint: ${AppLink.endPoint}'); - // Log.print('BoxName.serverChosen: ${box.read(BoxName.serverChosen)}'); - - newStartPointLocation = passengerLocation; - newMyLocation = passengerLocation; - - // Resolve current location address - try { - getReverseGeocoding(passengerLocation).then((address) { - currentLocationString = address; - update(); - }); - } catch (e) { - Log.print('Error resolving current location: $e'); - } - - // Trigger offline map caching for a 10km radius - OfflineMapService.instance - .downloadRegion(passengerLocation, radiusKm: 10.0); - - speed = _locationData.speed!; - // //print location details - isLoading = false; - update(); - } - - void clearPolyline() { - polyLines.clear(); - update(); - } - - LatLngBounds calculateBounds(double lat, double lng, double radiusInMeters) { - const double earthRadius = 6378137.0; // Earth's radius in meters - - double latDelta = (radiusInMeters / earthRadius) * (180 / pi); - double lngDelta = - (radiusInMeters / (earthRadius * cos(pi * lat / 180))) * (180 / pi); - - double minLat = lat - latDelta; - double maxLat = lat + latDelta; - - double minLng = lng - lngDelta; - double maxLng = lng + lngDelta; - - // Ensure the latitude is between -90 and 90 - minLat = max(-90.0, minLat); - maxLat = min(90.0, maxLat); - - // Ensure the longitude is between -180 and 180 - minLng = (minLng + 180) % 360 - 180; - maxLng = (maxLng + 180) % 360 - 180; - - // Ensure the bounds are in the correct order - if (minLng > maxLng) { - double temp = minLng; - minLng = maxLng; - maxLng = temp; - } - - return LatLngBounds( - southwest: LatLng(minLat, minLng), - northeast: LatLng(maxLat, maxLng), - ); - } - - void onMapCreated(IntaleqMapController controller) { - mapController = controller; - update(); - } - - void onStyleLoaded() async { - Log.print('🗺️ Intaleq Map Style Loaded. Initializing...'); - isStyleLoaded = true; - _loadMapIcons(); - - // Smart Camera Reset logic: - if (mapController != null) { - if (markers.isNotEmpty && lastComputedBounds != null) { - await _safeAnimateCameraBounds(lastComputedBounds); - } else { - mapController!.animateCamera( - CameraUpdate.newLatLng(passengerLocation), - ); - } - } - update(); - } - - /// Safe wrapper for animateCamera Bounds to prevent native std::domain_error crash on iOS. - Future _safeAnimateCameraBounds(LatLngBounds? bounds, - {double left = 60, - double top = 60, - double right = 60, - double bottom = 60}) async { - if (bounds == null || mapController == null) return; - - try { - // Ensure the coordinates are valid - if (bounds.northeast.latitude == bounds.southwest.latitude && - bounds.northeast.longitude == bounds.southwest.longitude) { - Log.print( - '⚠️ _safeAnimateCameraBounds: Bounds are a single point, zooming to point instead.'); - await mapController - ?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 15)); - return; - } - - // Small delay to ensure iOS view layout is fully ready - await Future.delayed(const Duration(milliseconds: 200)); - - await mapController?.animateCamera( - CameraUpdate.newLatLngBounds( - bounds, - left: left, - top: top, - right: right, - bottom: bottom, - ), - ); - } catch (e) { - Log.print('❌ _safeAnimateCameraBounds CRASH PREVENTED: $e'); - // Final fallback to prevent device freeze - try { - await mapController - ?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 14)); - } catch (_) {} - } - } - - Future _loadMapIcons() async { - // Wait up to 3 seconds for the map style to finish loading - for (int i = 0; i < 15; i++) { - if (mapController != null && isStyleLoaded) break; - await Future.delayed(const Duration(milliseconds: 200)); - } - - if (mapController == null || !isStyleLoaded) { - Log.print( - '⚠️ _loadMapIcons: mapController or style not ready. Icons may not load.'); - } - - await _addMapImage(startIcon, 'assets/images/A.png'); - await _addMapImage(endIcon, 'assets/images/b.png'); - await _addMapImage(carIcon, 'assets/images/car.png'); - await _addMapImage(motoIcon, 'assets/images/moto.png'); - await _addMapImage(ladyIcon, 'assets/images/lady.png'); - await _addMapImage('picker_icon', 'assets/images/picker.png'); - // Waypoint markers - use moto1 & lady1 as colored waypoint icons - await _addMapImage('orange_marker', 'assets/images/moto1.png'); - await _addMapImage('violet_marker', 'assets/images/lady1.png'); - } - - Future _addMapImage(String id, String path) async { - try { - final ByteData bytes = await rootBundle.load(path); - // Resize car icons for better visibility on map (e.g. 120px) - final size = _getImageSize(id); - if (size != null && (id == carIcon || id == motoIcon || id == ladyIcon)) { - final resized = await _resizeImage(bytes.buffer.asUint8List(), size); - await mapController?.addImage(id, resized); - Log.print( - '✅ Successfully added resized map image: $id (${size}x${size})'); - } else { - await mapController?.addImage(id, bytes.buffer.asUint8List()); - Log.print('✅ Successfully added map image: $id'); - } - } catch (e) { - Log.print('❌ Error loading map icon $id: $e'); - } - } - - int? _getImageSize(String id) { - if (id == carIcon || id == motoIcon || id == ladyIcon) return 120; - return null; - } - - Future _resizeImage(Uint8List bytes, int size) async { - return await compute((Uint8List data) { - final image = img.decodeImage(data); - if (image == null) return data; - final resized = img.copyResize(image, width: size, height: size); - return Uint8List.fromList(img.encodePng(resized)); - }, bytes); - } - - // Wait up to 3 seconds for the map style to finish loading - - void updateCurrentLocationFromCamera(LatLng target) { - Log.print('📍 updateCurrentLocationFromCamera: $target'); - newMyLocation = target; - - if (startLocationFromMap == true) { - Log.print('📍 Updating startLocationFromMap to $target'); - newStartPointLocation = target; - } else if (passengerStartLocationFromMap == true) { - Log.print('📍 Updating passengerStartLocationFromMap to $target'); - newStartPointLocation = target; - } - - int waypointsLength = Get.find().wayPoints.length; - if (waypointsLength > 0 && - wayPointIndex >= 0 && - wayPointIndex < placesCoordinate.length) { - Log.print('📍 Updating wayPointIndex $wayPointIndex to $target'); - placesCoordinate[wayPointIndex] = - '${target.latitude},${target.longitude}'; - } - update(); - } - - String durationByPassenger = ''; - late DateTime newTime1 = DateTime.now(); - late DateTime timeFromDriverToPassenger = DateTime.now(); - String distanceByPassenger = ''; - late Duration durationFromDriverToPassenger; - double nearestDistance = double.infinity; - - Future getNearestDriverByPassengerLocation() async { - if (!rideConfirm) { - if (dataCarsLocationByPassenger != 'failure' && - dataCarsLocationByPassenger != null && - dataCarsLocationByPassenger['message'] != null && - dataCarsLocationByPassenger['message'].length > 0) { - double nearestDistance = double.infinity; // Initialize nearest distance - CarLocation? nearestCar; - - for (var i = 0; - i < dataCarsLocationByPassenger['message'].length; - i++) { - var carLocation = dataCarsLocationByPassenger['message'][i]; - // Log.print('carLocation: $carLocation'); - - try { - // Calculate distance between passenger's location and current driver's location - final distance = Geolocator.distanceBetween( - passengerLocation.latitude, - passengerLocation.longitude, - double.parse(carLocation['latitude']), - double.parse(carLocation['longitude']), - ); - - // Calculate duration assuming an average speed of 25 km/h (adjust as needed) - int durationToPassenger = (distance / 1000 / 25 * 3600).round(); - // Log.print('distance: $distance'); - // Log.print('durationToPassenger: $durationToPassenger'); - // Log.print('passengerLocation: $passengerLocation'); - // Log.print('carLocation: $carLocation'); - // Log.print('distance: $distance meters'); - // Log.print('durationToPassenger: $durationToPassenger seconds'); - // Update the UI with the distance and duration for each car - update(); - - // If this distance is smaller than the nearest distance found so far, update nearestCar - if (distance < nearestDistance) { - nearestDistance = distance; - - nearestCar = CarLocation( - distance: distance, - duration: durationToPassenger.toDouble(), - id: carLocation['driver_id'], - latitude: double.parse(carLocation['latitude']), - longitude: double.parse(carLocation['longitude']), - ); - // Log.print('nearestCar: $nearestCar'); - // Update the UI with the nearest driver - update(); - } - } catch (e) { - Log.print('Error calculating distance/duration: $e'); - } - } - - // Return the nearest car found - return nearestCar; - } - } - - // Return null if no drivers are found or if ride is confirmed - return null; - } - - getNearestDriverByPassengerLocationAPIGOOGLE() async { - if (polyLines.isEmpty || data.isEmpty) { - return null; // Early return if data is empty - } - if (!rideConfirm) { - double nearestDistance = double.infinity; - if (dataCarsLocationByPassenger != 'failure') { - if (dataCarsLocationByPassenger['message'].length > 0) { - for (var i = 0; - i < dataCarsLocationByPassenger['message'].length; - i++) { - var carLocation = dataCarsLocationByPassenger['message'][i]; - - // } - // isloading = true; - update(); - // Make API request to get exact distance and duration - String apiUrl = - '${AppLink.googleMapsLink}distancematrix/json?destinations=${carLocation['latitude']},${carLocation['longitude']}&origins=${passengerLocation.latitude},${passengerLocation.longitude}&units=metric&key=${AK.mapAPIKEY}'; - var response = await CRUD().getGoogleApi(link: apiUrl, payload: {}); - if (response != null && response['status'] == "OK") { - var data = response; - // Extract distance and duration from the response and handle accordingly - int distance1 = - data['rows'][0]['elements'][0]['distance']['value']; - distanceByPassenger = - data['rows'][0]['elements'][0]['distance']['text']; - durationToPassenger = - data['rows'][0]['elements'][0]['duration']['value']; - - durationFromDriverToPassenger = - Duration(seconds: durationToPassenger.toInt()); - newTime1 = currentTime.add(durationFromDriverToPassenger); - timeFromDriverToPassenger = - newTime1.add(Duration(minutes: 2.toInt())); - durationByPassenger = - data['rows'][0]['elements'][0]['duration']['text']; - update(); - if (distance1 < nearestDistance) { - nearestDistance = distance1.toDouble(); - - nearestCar = CarLocation( - distance: distance1.toDouble(), - duration: durationToPassenger.toDouble(), - id: carLocation['driver_id'], - latitude: double.parse(carLocation['latitude']), - longitude: double.parse(carLocation['longitude']), - ); - // isloading = false; - update(); - } - } - - // Handle the distance and duration as needed - else { - // 'Failed to retrieve distance and duration: ${response['status']}'); - Log.print('${response['status']}: ${response['status']}}'); - // Handle the failure case - } - } - } - } - } - } - - calculateDistanceBetweenPassengerAndDriverBeforeCancelRide() async { - await getDriverCarsLocationToPassengerAfterApplied(); - double distance = Geolocator.distanceBetween( - passengerLocation.latitude, - passengerLocation.longitude, - driverCarsLocationToPassengerAfterApplied.last.latitude, - driverCarsLocationToPassengerAfterApplied.last.longitude, - ); - if (distance > 500) { - isCancelRidePageShown = true; - update(); - } else { - Get.defaultDialog( - barrierDismissible: false, - title: 'The Driver Will be in your location soon .'.tr, - middleText: 'The distance less than 500 meter.'.tr, - confirm: Column( - children: [ - MyElevatedButton( - kolor: AppColor.greenColor, - title: 'Ok'.tr, - onPressed: () { - Get.back(); - }, - ), - MyElevatedButton( - kolor: AppColor.redColor, - title: 'No, I want to cancel this trip'.tr, - onPressed: () { - Get.back(); - MyDialog().getDialog( - 'Attention'.tr, - 'You will be charged for the cost of the driver coming to your location.' - .tr, - () async { - Get.back(); - Get.find() - .payToDriverForCancelAfterAppliedAndHeNearYou(rideId); - // isCancelRidePageShown = true; - // update(); - }, - ); - }, - ), - ], - ), - ); - // cancel: MyElevatedButton( - // title: 'No.Iwant Cancel Trip.'.tr, onPressed: () {})); - } - } - - List headingAngles = []; - double calculateAngleBetweenLocations(LatLng start, LatLng end) { - double startLat = start.latitude * math.pi / 180; - double startLon = start.longitude * math.pi / 180; - double endLat = end.latitude * math.pi / 180; - double endLon = end.longitude * math.pi / 180; - - double dLon = endLon - startLon; - - double y = math.sin(dLon) * cos(endLat); - double x = cos(startLat) * math.sin(endLat) - - math.sin(startLat) * cos(endLat) * cos(dLon); - - double angle = math.atan2(y, x); - double angleDegrees = angle * 180 / math.pi; - - return angleDegrees; - } - - late LatLngBounds boundsData; - late String startNameAddress = ''; - late String endNameAddress = ''; - List> stopPoints = []; - void removeStop(Map stop) { - stopPoints.remove(stop); - update(); // Trigger a rebuild of the UI - } - - Future getReverseGeocoding(LatLng location) async { - final lat = location.latitude; - final lng = location.longitude; - final url = '${AppLink.reverseGeocoding}?lat=$lat&lng=$lng'; - - try { - final response = await CRUD().getMapSaas(link: url); - - if (response != null && response is List && response.isNotEmpty) { - final data = response[0]; - String name = data['name_ar'] ?? data['name'] ?? 'Unknown Location'.tr; - return name; - } - return 'Unknown Location'.tr; - } catch (e) { - Log.print('ReverseGeocoding Exception: $e'); - return 'Unknown Location'.tr; - } - } - - bool isDrawingRoute = false; - void showDrawingBottomSheet() { - Log.print( - '🔔 showDrawingBottomSheet called. isDrawingRoute: $isDrawingRoute'); - - final context = Get.context; - if (context == null) return; - - WidgetsBinding.instance.addPostFrameCallback((_) { - // Close any existing open dialogs first - if (Get.isDialogOpen == true) { - Get.back(); - } - - Get.dialog( - Dialog( - backgroundColor: Colors.transparent, - elevation: 0, - child: Container( - padding: const EdgeInsets.all(24), - width: 180, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.95), - borderRadius: BorderRadius.circular(24), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.15), - blurRadius: 20, - spreadRadius: 5, - ) - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // App Logo - Image.asset( - 'assets/images/logo.gif', - height: 64, - errorBuilder: (context, error, stackTrace) => const Icon( - Icons.map, - size: 64, - color: AppColor.primaryColor, - ), - ), - const SizedBox(height: 16), - const SizedBox( - width: 24, - height: 24, - child: MyCircularProgressIndicator(), - ), - const SizedBox(height: 16), - Text( - 'Drawing route on map...'.tr, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - color: AppColor.primaryColor, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - barrierDismissible: false, - ); - - // Auto-dismiss after exactly 2 seconds - Future.delayed(const Duration(seconds: 2), () { - if (Get.isDialogOpen == true) { - Get.back(); - } - }); - }); - } - - String dynamicApiUrl = 'https://routec.intaleq.xyz/route'; - Future getDistanceFromDriverAfterAcceptedRide( - String origin, String destination) async { - String apiKey = Env.mapKeyOsm; // مفتاح API الخاص بك - if (origin.isEmpty) { - origin = '${passengerLocation.latitude},${passengerLocation.longitude}'; - } - // 2. بناء الرابط (URI) - // Waypoints غير مدعومة حالياً في OSRM، لذلك تم تجاهلها - var uri = Uri.parse( - '$dynamicApiUrl?origin=$origin&destination=$destination&steps=false&overview=false'); - Log.print('uri: $uri'); - - // 3. إرسال الطلب مع الهيدر - http.Response response; - Map responseData; - - try { - response = await http.get( - uri, - headers: { - 'X-API-KEY': apiKey, - }, - ).timeout(const Duration(seconds: 20)); // تايم آوت 20 ثانية - - if (response.statusCode != 200) { - Log.print('Error from API: ${response.statusCode}'); - isLoading = false; - update(); - return; // خروج في حالة الخطأ - } - if (Get.isBottomSheetOpen ?? false) { - Get.back(); // لإغلاق شاشة "جاري الرسم" - } - isDrawingRoute = false; // Reset state - - responseData = json.decode(response.body); - Log.print('responseData: $responseData'); - - if (responseData['status'] != 'ok') { - Log.print('API returned an error: ${responseData['message']}'); - isLoading = false; - update(); - return; // خروج في حالة خطأ منطقي (مثل "no path") - } - } catch (e) { - Log.print('Failed to get directions: $e'); - isLoading = false; - update(); - return; // خروج عند فشل الاتصال - } - } - - // (b = 1.5348) هو المعامل الذي تم حسابه من مقارنة 60 رحلة بين Google و OSRM - double kDurationScalar = - 1.5348; //this from colab 60 random locations from google and routec - -// ----------------------------------------------------------------------------------------- - // GET DIRECTION MAP (FULL) - // ----------------------------------------------------------------------------------------- - // ----------------------------------------------------------------------------------------- - // GET DIRECTION MAP (With Auto-Retry Logic) - // ----------------------------------------------------------------------------------------- - // أضفنا attemptCount لتتبع عدد المحاولات - // ----------------------------------------------------------------------------------------- - // GET DIRECTION MAP (Retry or Fail Strict Logic) - // ----------------------------------------------------------------------------------------- - Future getDirectionMap(String origin, String destination, - [List waypoints = const [], int attemptCount = 0]) async { - // 1. إظهار التحميل فقط في المحاولة الأولى - if (attemptCount == 0) { - // NOTE: Do NOT set isLoading = true here! - // isLoading destroys the MapLibreMap widget entirely (replaced by spinner), - // which means markers/polylines cannot be added to the new map instance - // until its style finishes loading asynchronously — causing a race condition. - // The showDrawingBottomSheet() overlay provides sufficient user feedback. - isDrawingRoute = true; - update(); - if (isDrawingRoute) showDrawingBottomSheet(); - - await getCarsLocationByPassengerAndReloadMarker(); - } - - // تجهيز الإحداثيات - if (origin.isEmpty) { - origin = '${passengerLocation.latitude},${passengerLocation.longitude}'; - } - - var coordDestination = destination.split(','); - double latDest = double.parse(coordDestination[0]); - double lngDest = double.parse(coordDestination[1]); - myDestination = LatLng(latDest, lngDest); - - // ── 2. Unified SaaS Routing Strategy ────────────────────────── - final bool isSaaSRequest = true; - Uri uri; - - var originCoords = origin.split(','); - final Map queryParams = { - 'fromLat': originCoords[0].trim(), - 'fromLng': originCoords[1].trim(), - 'toLat': latDest.toString(), - 'toLng': lngDest.toString(), - }; - - // Add multi-stop waypoints to the query parameters - for (int i = 0; i < activeMenuWaypointCount; i++) { - final wp = menuWaypoints[i]; - if (wp != null) { - queryParams['stop${i + 1}Lat'] = wp.latitude.toString(); - queryParams['stop${i + 1}Lng'] = wp.longitude.toString(); - } - } - - uri = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams); - - Log.print( - 'Requesting Route URI (${isSaaSRequest ? "SaaS" : "OSRM"}, Attempt: ${attemptCount + 1}): $uri'); - - http.Response response; - Map responseData; - - try { - response = await http.get(uri, headers: { - 'x-api-key': Env.mapSaasKey, - }).timeout(const Duration(seconds: 20)); - - responseData = json.decode(response.body); - - // Validation: SaaS returns 200 with data, OSRM returns code: 'Ok' - bool isRequestValid = response.statusCode == 200 && - (isSaaSRequest || responseData['code'] == 'Ok'); - - if (!isRequestValid) { - if (attemptCount < 2) { - await _retryProcess(origin, destination, waypoints, attemptCount); - return; - } - _handleFatalError( - "Server Error".tr, "Connection failed. Please try again.".tr); - return; - } - - // ============================================================ - // 🛑 الفحص الأمني (Sanity Check) - // ============================================================ - double apiDistanceMeters; - String pointsString; - dynamic routeData; - - // SaaS parsing - apiDistanceMeters = (responseData['distance'] as num).toDouble(); - pointsString = responseData['points'] ?? ""; - routeData = responseData; // For box storage - - var originCoords = origin.split(','); - double startLat = double.parse(originCoords[0]); - double startLng = double.parse(originCoords[1]); - - // المسافة الجوية - double aerialDistance = - Geolocator.distanceBetween(startLat, startLng, latDest, lngDest); - - if (apiDistanceMeters < 50.0 && aerialDistance > 200.0) { - Log.print( - "⚠️ Suspicious Route detected! Server: $apiDistanceMeters m | Aerial: $aerialDistance m"); - - if (attemptCount < 2) { - Log.print("🔄 Retrying request (Attempt ${attemptCount + 2})..."); - await Future.delayed(const Duration(seconds: 1)); - await getDirectionMap( - origin, destination, waypoints, attemptCount + 1); - return; - } else { - Log.print("❌ All retries failed. Calculating Route is impossible."); - _handleFatalError( - "Route Not Found".tr, - "We couldn't find a valid route to this destination. Please try selecting a different point." - .tr); - return; - } - } - - // 3. معالجة البيانات - box.remove(BoxName.tripData); - box.write(BoxName.tripData, routeData); - - durationToRide = - ((routeData['duration'] as num) * kDurationScalar).toInt(); - double distanceOfTrip = apiDistanceMeters / 1000.0; - distance = distanceOfTrip; - - data = routeData['legs'] != null && routeData['legs'].isNotEmpty - ? (routeData['legs'][0]['steps'] ?? []) - : []; - - List decodedPoints = []; - if (pointsString.isNotEmpty) { - decodedPoints = await compute(decodePolylineIsolate, pointsString); - } - - if (decodedPoints.isEmpty) { - _handleFatalError("Map Error".tr, "Received empty route data.".tr); - return; - } - - polylineCoordinates.clear(); - polylineCoordinates.addAll(decodedPoints); - - final LatLng startLoc = polylineCoordinates.first; - final LatLng endLoc = polylineCoordinates.last; - - // ── 4. العناوين والتحديثات ────────────────────────────────── - startNameAddress = responseData['startName'] ?? 'Start Point'.tr; - endNameAddress = responseData['endName'] ?? 'Destination'.tr; - Log.print('📍 ROUTE START: $startNameAddress'); - Log.print('📍 ROUTE END: $endNameAddress'); - - // ── 5. Bounds Calculation (SaaS bbox vs OSRM manual) ────────── - if (isSaaSRequest && responseData['bbox'] != null) { - List bbox = responseData['bbox']; - if (bbox.length == 4) { - // SaaS format: [minLng, minLat, maxLng, maxLat] - lastComputedBounds = LatLngBounds( - southwest: LatLng(bbox[1], bbox[0]), - northeast: LatLng(bbox[3], bbox[2]), - ); - } - } else { - double? minLat, maxLat, minLng, maxLng; - for (LatLng point in polylineCoordinates) { - minLat = - minLat == null ? point.latitude : min(minLat, point.latitude); - maxLat = - maxLat == null ? point.latitude : max(maxLat, point.latitude); - minLng = - minLng == null ? point.longitude : min(minLng, point.longitude); - maxLng = - maxLng == null ? point.longitude : max(maxLng, point.longitude); - } - if (minLat != null) { - lastComputedBounds = LatLngBounds( - northeast: LatLng(maxLat!, maxLng!), - southwest: LatLng(minLat!, minLng!)); - } - } - // isDrawingRoute = false; - // 5b. Reset state when finished - if (isDrawingRoute) { - Log.print('🔔 Finalizing route drawing state'); - isDrawingRoute = false; - isLoading = false; - update(); - } - - // 6. إضافة الماركرز - durationToAdd = Duration(seconds: durationToRide); - hours = durationToAdd.inHours; - minutes = (durationToAdd.inMinutes % 60).round(); - - markers = { - Marker( - markerId: const MarkerId('start'), - position: startLoc, - icon: InlqBitmap.fromStyleImage('orange_marker'), - infoWindow: const InfoWindow(title: 'A'), - anchor: const Offset(0.5, 1.0), - ), - Marker( - markerId: const MarkerId('end'), - position: endLoc, - icon: InlqBitmap.fromStyleImage('violet_marker'), - infoWindow: const InfoWindow(title: 'B'), - anchor: const Offset(0.5, 1.0), - ), - }; - - for (int i = 0; i < activeMenuWaypointCount; i++) { - final wp = menuWaypoints[i]; - if (wp != null) { - final bool isFirstWaypoint = i == 0; - markers.add(Marker( - markerId: MarkerId('waypoint_$i'), - position: wp, - icon: InlqBitmap.fromStyleImage( - isFirstWaypoint ? 'orange_marker' : 'violet_marker'), - infoWindow: - InfoWindow(title: isFirstWaypoint ? 'Stop 1' : 'Stop 2'), - anchor: const Offset(0.5, 1.0), - )); - } - } - - // 7. رسم الخط - if (polyLines.isNotEmpty) clearPolyline(); - - rideConfirm = false; - isMarkersShown = true; - update(); // تحديث أولي لإظهار الخريطة والماركرز - - // إظهار الباتم شيت للسعر - await bottomSheet(); - - // تشغيل الأنيميشن الخفيف لومضات المسار + fit camera after - await _playRouteAnimation(polylineCoordinates, lastComputedBounds); - } catch (e, stackTrace) { - // 🚨 Cleanup on error to prevent UI freeze - if (isDrawingRoute) { - isDrawingRoute = false; - isLoading = false; - update(); - } - - Log.print('🚨 CRITICAL ERROR IN getDirectionMap: $e'); - Log.print('🚨 STACKTRACE: $stackTrace'); - - if (attemptCount < 2) { - await _retryProcess(origin, destination, waypoints, attemptCount); - } else { - _handleFatalError("Connection Error".tr, - "Please check your internet and try again.".tr); - } - } - } - - // --- رسم المسار النهائي مع تقسيم ملون حسب نقاط التوقف --- - Future _playRouteAnimation( - List coords, LatLngBounds? bounds) async { - // Segment colors matching UI dots: green → amber → purple → red - const List segmentColors = [ - Color(0xFF109642), // Green (start → stop 1) - Color(0xFFF59E0B), // Amber (stop 1 → stop 2) - Color(0xFF7C3AED), // Purple (last segment → dest) - Color(0xFFEF4444), // Red (fallback) - ]; - - // ── Build final polyline segments ─────────────────────────────────── - // Build all segments in a temporary Set first, then assign once - Set newPolylines = {}; - - if (activeMenuWaypointCount > 0) { - List splitIndices = []; - for (int w = 0; w < activeMenuWaypointCount; w++) { - final wp = menuWaypoints[w]; - if (wp == null) continue; - int bestIdx = 0; - double bestDist = double.infinity; - for (int j = 0; j < coords.length; j++) { - final dx = coords[j].latitude - wp.latitude; - final dy = coords[j].longitude - wp.longitude; - final d = dx * dx + dy * dy; - if (d < bestDist) { - bestDist = d; - bestIdx = j; - } - } - splitIndices.add(bestIdx); - } - splitIndices.sort(); - - List boundaries = [0, ...splitIndices, coords.length - 1]; - for (int s = 0; s < boundaries.length - 1; s++) { - int from = boundaries[s]; - int to = boundaries[s + 1] + 1; - if (to > coords.length) to = coords.length; - if (from >= to - 1) continue; - final segCoords = coords.sublist(from, to); - if (segCoords.length < 2) continue; - final color = segmentColors[s % segmentColors.length]; - - newPolylines.add(Polyline( - polylineId: PolylineId('segment_$s'), - points: segCoords, - color: color, - width: 6, - )); - } - } else { - newPolylines.add(Polyline( - polylineId: const PolylineId('route_primary'), - points: coords, - color: AppColor.primaryColor, - width: 6, - )); - } - - polyLines = newPolylines; - update(); - - Log.print( - '🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map'); - - update(); - - // ── Fit camera to full route bounds ──────────────────────────────── - if (bounds != null) { - await _safeAnimateCameraBounds(bounds); - } - } - - // --- دالة المساعدة لإعادة المحاولة --- - Future _retryProcess(String origin, String dest, List waypoints, - int currentAttempt) async { - Log.print( - "🔄 Exception or Error caught. Retrying in 1s... (Attempt ${currentAttempt + 1})"); - await Future.delayed(const Duration(seconds: 1)); - getDirectionMap(origin, dest, waypoints, currentAttempt + 1); - } - -// --- دالة جديدة لتنظيف الخريطة بالكامل ومنع تداخل الرحلات --- - void resetAllMapStates() { - Log.print('🧹 Resetting all map states to prevent sticky location bug'); - - clearPlacesDestination(); - clearPlacesStart(); - clearPolyline(); - data = []; - - passengerStartLocationFromMap = false; - startLocationFromMap = false; - isPickerShown = false; - workLocationFromMap = false; - homeLocationFromMap = false; - isAnotherOreder = false; - isWhatsAppOrder = false; - - // ✅ أضف هذا: reset الوجهة لموقع الراكب حتى لا تبقى قيمة الرحلة القديمة - myDestination = passengerLocation; - hintTextDestinationPoint = 'Select your destination'.tr; - - placeDestinationController.clear(); - placeStartController.clear(); - - rideConfirm = false; - shouldFetch = false; - isDrawingRoute = false; - isLoading = false; - - update(); - } - - // ----------------------------------------------------------------------------------------- - // 🛑 دالة الخطأ القاتل (تغلق كل شيء وتعيد المستخدم للخريطة) - // ----------------------------------------------------------------------------------------- - void _handleFatalError(String title, String message) { - // 1. إغلاق شاشة التحميل (Drawing route...) - if (Get.isBottomSheetOpen == true || Get.isDialogOpen == true) { - Get.back(); - } - if (Get.isSnackbarOpen) Get.closeCurrentSnackbar(); - - // 2. تصفير المتغيرات - isDrawingRoute = false; - isLoading = false; - update(); - - // 3. إظهار الديالوج الإجباري - Get.defaultDialog( - title: title, - titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), - middleText: message, - middleTextStyle: AppStyle.subtitle, - barrierDismissible: false, // لا يمكن إغلاقه بالضغط خارجاً - confirm: MyElevatedButton( - title: "Close".tr, - kolor: AppColor.redColor, - onPressed: () { - Get.back(); // إغلاق الديالوج - - // 4. إعادة تحميل الصفحة بالكامل (تنظيف الحالة) - // تأكد من استيراد MapPagePassenger - Get.offAll(() => const MapPagePassenger()); - }, - ), - ); - } - - // Legacy gradient and layered animations removed for MapLibre migration - - String shortenAddress(String fullAddress) { - // Split the address into parts - List parts = fullAddress.split('،'); - - // Remove any leading or trailing whitespace from each part - parts = parts.map((part) => part.trim()).toList(); - - // Remove any empty parts - parts = parts.where((part) => part.isNotEmpty).toList(); - - // Initialize the short address - String shortAddress = ''; - - if (parts.isNotEmpty) { - // Add the first part (usually the most specific location) - shortAddress += parts[0]; - } - - if (parts.length > 2) { - // Add the district or area name (usually the third part in Arabic format) - shortAddress += '، ${parts[2]}'; - } else if (parts.length > 1) { - // Add the second part for English or shorter addresses - shortAddress += '، ${parts[1]}'; - } - - // Add the country (usually the last part) - if (parts.length > 1) { - shortAddress += '، ${parts.last}'; - } - - // Remove any part that's just numbers (like postal codes) - shortAddress = shortAddress - .split('،') - .where((part) => !RegExp(r'^[0-9 ]+$').hasMatch(part.trim())) - .join('،'); - - // Check if the address is in English - bool isEnglish = - RegExp(r'^[a-zA-Z0-9 ]+$').hasMatch(shortAddress.replaceAll('،', '')); - - if (isEnglish) { - // Further processing for English addresses - List englishParts = shortAddress.split('،'); - if (englishParts.length > 2) { - shortAddress = - '${englishParts[0]}، ${englishParts[1]}، ${englishParts.last}'; - } else if (englishParts.length > 1) { - shortAddress = '${englishParts[0]}، ${englishParts.last}'; - } - } - - return shortAddress; - } - - double distanceOfDestination = 0; - bool haveSteps = false; - late LatLng latestPosition; - - getMapPoints(String originSteps, String destinationSteps, int index) async { - isWayPointStopsSheetUtilGetMap = false; - // haveSteps = true; - // startCarLocationSearch(box.read(BoxName.carType)); - await getCarsLocationByPassengerAndReloadMarker(); - // await getCarsLocationByPassengerAndReloadMarker(); - // isLoading = true; - update(); - var url = - ('${AppLink.googleMapsLink}directions/json?&language=${box.read(BoxName.lang)}&avoid=tolls|ferries&destination=$destinationSteps&origin=$originSteps&key=${AK.mapAPIKEY}'); - var response = await CRUD().getGoogleApi(link: url, payload: {}); - - data = response['routes'][0]['legs']; - // isLoading = false; - - int durationToRide0 = data[0]['duration']['value']; - durationToRide = durationToRide + durationToRide0; - distance = distanceOfDestination + (data[0]['distance']['value']) / 1000; - - update(); - // final points = - // decodePolyline(response["routes"][0]["overview_polyline"]["points"]); - final String pointsString = - response['routes'][0]["overview_polyline"]["points"]; - - List decodedPoints = - await compute(decodePolylineIsolate, pointsString); - // decodePolyline(response["routes"][0]["overview_polyline"]["points"]); - for (int i = 0; i < decodedPoints.length; i++) { - polylineCoordinates.add(decodedPoints[i]); - } -// Define the northeast and southwest coordinates - - if (polyLines.isEmpty) { - var polyline = Polyline( - polylineId: PolylineId('route_$index'), - points: polylineCoordinatesPointsAll[index], - width: 6, - color: const Color(0xFF2196F3), - ); - - polyLines = {...polyLines, polyline}; - rideConfirm = false; - update(); - } - } - - void updateCameraForDistanceAfterGetMap() { - LatLng coord1 = LatLng( - double.parse(coordinatesWithoutEmpty.first.split(',')[0]), - double.parse(coordinatesWithoutEmpty.first.split(',')[1])); - - LatLng coord2 = LatLng( - double.parse(coordinatesWithoutEmpty.last.split(',')[0]), - double.parse(coordinatesWithoutEmpty.last.split(',')[1])); - - LatLng northeast; - LatLng southwest; - - if (coord1.latitude > coord2.latitude) { - northeast = coord1; - southwest = coord2; - } else { - northeast = coord2; - southwest = coord1; - } - - // Create the LatLngBounds object - LatLngBounds bounds = - LatLngBounds(northeast: northeast, southwest: southwest); - - // Fit the camera to the bounds - var cameraUpdate = CameraUpdate.newLatLngBounds(bounds, - left: 180, top: 180, right: 180, bottom: 180); - mapController!.animateCamera(cameraUpdate); - update(); - } - - int selectedIndex = -1; // Initialize with no selection - void selectCarFromList(int index) { - selectedIndex = index; // Update selected index - carTypes.forEach( - (element) => element.isSelected = false); // Reset selection flags - carTypes[index].isSelected = true; - update(); - } - - showBottomSheet1() async { - await bottomSheet(); - isBottomSheetShown = true; - heightBottomSheetShown = 250; - - update(); - } - - final promo = TextEditingController(); - bool promoTaken = false; - void applyPromoCodeToPassenger(BuildContext context) async { - if (promoTaken == true) { - MyDialog().getDialog( - 'Promo Already Used'.tr, - 'You have already used this promo code.'.tr, - () => Get.back(), - ); - return; - } - - if (!promoFormKey.currentState!.validate()) return; - - // العتبات بالليرة السورية - const double minPromoLowSYP = 172; // Speed / Balash - const double minPromoHighSYP = 200; // Comfort / Electric / Lady - - try { - final value = await CRUD().get( - link: AppLink.getPassengersPromo, - payload: {'promo_code': promo.text}, - ); - - if (value == 'failure') { - MyDialog().getDialog( - 'Promo Ended'.tr, - 'The promotion period has ended.'.tr, - () => Get.back(), - ); - return; - } - - // هل يوجد فئة مؤهلة أصلاً قبل الخصم؟ - final bool eligibleNow = (totalPassengerSpeed >= minPromoLowSYP) || - (totalPassengerBalash >= minPromoLowSYP) || - (totalPassengerComfort >= minPromoHighSYP) || - (totalPassengerElectric >= minPromoHighSYP) || - (totalPassengerLady >= minPromoHighSYP); - - if (!eligibleNow) { - Get.snackbar( - 'Lowest Price Achieved'.tr, - 'Cannot apply further discounts.'.tr, - backgroundColor: AppColor.yellowColor, - ); - return; - } - - final decode = jsonDecode(value); - if (decode["status"] != "success") { - MyDialog().getDialog( - 'Promo Ended'.tr, - 'The promotion period has ended.'.tr, - () => Get.back(), - ); - return; - } - - Get.snackbar('Promo Code Accepted'.tr, '', - backgroundColor: AppColor.greenColor); - - final firstElement = decode["message"][0]; - final int discountPercentage = - int.tryParse(firstElement['amount'].toString()) ?? 0; - - // قيمة المحفظة - قد تكون سالبة - final double walletVal = double.tryParse( - box.read(BoxName.passengerWalletTotal)?.toString() ?? '0') ?? - 0.0; - - final bool isWalletNegative = walletVal < 0; - - // -------------------------- - // دالة تُطبّق الخصم دون النزول تحت الحد الأدنى - // -------------------------- - double _applyDiscountPerTier({ - required double fare, - required double minThreshold, - required bool isWalletNegative, - }) { - if (fare < minThreshold) return fare; // غير مؤهل أصلاً - - final double discount = fare * (discountPercentage / 100.0); - double result; - - if (isWalletNegative) { - double neg = (-1) * walletVal; // walletVal < 0 => neg positive - result = fare + neg - discount; - } else { - result = fare - discount; - } - - // لا نسمح بالنزول دون الحد الأدنى - if (result < minThreshold) { - result = minThreshold; - } - - // ولا نسمح بمبلغ سالب - return result.clamp(0.0, double.infinity); - } - - // Comfort - totalPassengerComfort = _applyDiscountPerTier( - fare: totalPassengerComfort, - minThreshold: minPromoHighSYP, - isWalletNegative: isWalletNegative, - ); - - // Electric - totalPassengerElectric = _applyDiscountPerTier( - fare: totalPassengerElectric, - minThreshold: minPromoHighSYP, - isWalletNegative: isWalletNegative, - ); - - // Lady - totalPassengerLady = _applyDiscountPerTier( - fare: totalPassengerLady, - minThreshold: minPromoHighSYP, - isWalletNegative: isWalletNegative, - ); - - // Speed - totalPassengerSpeed = _applyDiscountPerTier( - fare: totalPassengerSpeed, - minThreshold: minPromoLowSYP, - isWalletNegative: isWalletNegative, - ); - - // Balash - totalPassengerBalash = _applyDiscountPerTier( - fare: totalPassengerBalash, - minThreshold: minPromoLowSYP, - isWalletNegative: isWalletNegative, - ); - - // تعديل دخل السائق وفق نسبة الخصم - totalDriver = totalDriver - (totalDriver * discountPercentage / 100.0); - - promoTaken = true; - update(); - - // مؤثرات - Confetti.launch( - context, - options: const ConfettiOptions(particleCount: 100, spread: 70, y: 0.6), - ); - - Get.back(); - await Future.delayed(const Duration(milliseconds: 120)); - } catch (e) { - Get.snackbar('Error'.tr, e.toString(), - backgroundColor: AppColor.redColor); - } - } - - double getDistanceFromText(String distanceText) { - // Remove any non-digit characters from the distance text - String distanceValue = distanceText.replaceAll(RegExp(r'[^0-9.]+'), ''); - - // Parse the extracted numerical value as a double - double distance = double.parse(distanceValue); - - return distance; - } - - double costForDriver = 0; - double totalPassengerSpeed = 0; - double totalPassengerBalash = 0; - double totalPassengerElectric = 0; - double totalPassengerLady = 0; - double totalPassengerRayehGai = 0; - double totalPassengerRayehGaiComfort = 0; - double totalPassengerRayehGaiBalash = 0; - Future bottomSheet() async { - // if (data.isEmpty) return; - - // === إعدادات عامة === - const double minFareSYP = 160; // حد أدنى - const double minBillableKm = 0.3; // حد أدنى للمسافة المفوترة - const double ladyFlatAddon = 20; // إضافة ثابتة لـ Lady - const double airportAddonSYP = 200; // إضافة المطار - - // --- ⬇️ الإضافة الجديدة: إضافة حدود مطار دمشق ⬇️ --- - const double damascusAirportBoundAddon = 1400; // إضافة المطار (حدود) - // --- ⬆️ نهاية الإضافة ⬆️ --- - - // كهرباء - const double electricPerKmUplift = 4; // زيادة/كم - const double electricFlatAddon = 10; // زيادة ثابتة - - // Long Speed - const double longSpeedThresholdKm = 40.0; - const double longSpeedPerKm = 26.0; // Speed عند >40كم - - // قواعد الرحلات البعيدة للدقائق (تعمل لكل الأوقات) - const double mediumDistThresholdKm = 25.0; // >25كم - const double longDistThresholdKm = 35.0; // >35كم - const double longTripPerMin = 6.0; - const int minuteCapMedium = 60; // سقف دقائق عند >25كم - const int minuteCapLong = 80; // سقف دقائق عند >35كم - const int freeMinutesLong = 10; // عفو 10 دقائق عند >35كم - - // تخفيضات المسافات الكبيرة للفئات غير Speed - const double extraReduction100 = 0.07; // +7% فوق تخفيض >40كم للرحلات >100كم - const double maxReductionCap = 0.35; // سقف 35% كحد أقصى - - // ====== زمن الرحلة ====== - durationToAdd = Duration(seconds: durationToRide); - hours = durationToAdd.inHours; - minutes = (durationToAdd.inMinutes % 60).round(); - final DateTime currentTime = DateTime.now(); - newTime = currentTime.add(durationToAdd); - averageDuration = (durationToRide / 60) / distance; - // +5 minutes per waypoint stop surcharge - final int waypointSurchargeMinutes = activeMenuWaypointCount * 5; - final int totalMinutes = - (durationToRide / 60).floor() + waypointSurchargeMinutes; - - // ====== أدوات مساعدة ====== - bool _isAirport(String s) { - final t = s.toLowerCase(); - return t.contains('airport') || - s.contains('مطار') || - s.contains('المطار'); - } - - bool _isClub(String s) { - final t = s.toLowerCase(); - return t.contains('club') || - t.contains('nightclub') || - t.contains('night club') || - s.contains('ديسكو') || - s.contains('ملهى ليلي'); - } - - // --- ⬇️ الإضافة الجديدة: دالة التحقق من حدود المطار ⬇️ --- - // (P1: 33.415313, 36.499687) (P2: 33.400265, 36.531505) - bool _isInsideDamascusAirportBounds(double lat, double lng) { - final double northLat = 33.415313; - final double southLat = 33.400265; - final double eastLng = 36.531505; - final double westLng = 36.499687; - - // التحقق من خط العرض (بين الشمال والجنوب) - bool isLatInside = (lat <= northLat) && (lat >= southLat); - // التحقق من خط الطول (بين الشرق والغرب) - bool isLngInside = (lng <= eastLng) && (lng >= westLng); - - return isLatInside && isLngInside; - } - // --- ⬆️ نهاية الإضافة ⬆️ --- - - // أسعار الدقيقة من السيرفر - final double naturePerMin = naturePrice; // طبيعي - final double latePerMin = latePrice; // ليل - final double heavyPerMin = heavyPrice; // ذروة - - // سعر الدقيقة حسب الوقت (أساس قبل قواعد المسافة) - double _perMinuteByTime(DateTime now, bool clubCtx) { - final h = now.hour; - if (h >= 21 || h < 1) return latePerMin; // ليل - if (h >= 1 && h < 5) return clubCtx ? (latePerMin * 2) : latePerMin; - if (h >= 14 && h <= 17) return heavyPerMin; // ذروة - return naturePerMin; // طبيعي - } - - // حد أدنى - double _applyMinFare(double fare) => - (fare < minFareSYP) ? minFareSYP : fare; - - // عمولة الراكب (kazan من السيرفر) - double _withCommission(double base) => - (base * (1 + kazan / 100)).ceilToDouble(); - - // ====== سياق ====== - final bool airportCtx = - _isAirport(startNameAddress) || _isAirport(endNameAddress); - final bool clubCtx = _isClub(startNameAddress) || _isClub(endNameAddress); - - // --- ⬇️ الإضافة الجديدة: التحقق من سياق حدود المطار ⬇️ --- - // !! ⚠️ تأكد من أن هذه هي المتغيرات الصحيحة لإحداثيات نقطة النهاية !! - double destLat = 0.0; - double destLng = 0.0; - try { - destLat = myDestination.latitude; - destLng = myDestination.longitude; - } catch (_) { - if (coordinatesWithoutEmpty.isNotEmpty) { - destLat = - double.tryParse(coordinatesWithoutEmpty.last.split(',')[0]) ?? 0.0; - destLng = - double.tryParse(coordinatesWithoutEmpty.last.split(',')[1]) ?? 0.0; - } - } - - final bool damascusAirportBoundCtx = _isInsideDamascusAirportBounds( - destLat, - destLng, - ); - final bool isInDamascusAirportBoundCtx = _isInsideDamascusAirportBounds( - newMyLocation.latitude.toDouble(), // <-- ⚠️ غيّر هذا للمتغير الصحيح - newMyLocation.longitude.toDouble(), // <-- ⚠️ غيّر هذا للمتغير الصحيح - ); - // --- ⬆️ نهاية الإضافة ⬆️ --- - - // ====== مسافة مفوترة ====== - final double billableDistance = - (distance < minBillableKm) ? minBillableKm : distance; - - // ====== Speed (قصير/طويل) ====== - final bool isLongSpeed = billableDistance > longSpeedThresholdKm; - final double perKmSpeedBaseFromServer = - speedPrice; // مثال: 2900 يأتي من السيرفر - final double perKmSpeed = - isLongSpeed ? longSpeedPerKm : perKmSpeedBaseFromServer; - - // ====== تخفيضات الفئات الأخرى حسب بُعد الرحلة ====== - // ... (الكود كما هو) ... - double reductionPct40 = 0.0; - if (perKmSpeedBaseFromServer > 0) { - reductionPct40 = (1.0 - (longSpeedPerKm / perKmSpeedBaseFromServer)) - .clamp(0.0, maxReductionCap); - } - final double reductionPct100 = - (reductionPct40 + extraReduction100).clamp(0.0, maxReductionCap); - double distanceReduction = 0.0; - if (billableDistance > 100.0) { - distanceReduction = reductionPct100; - } else if (billableDistance > 40.0) { - distanceReduction = reductionPct40; - } - - // ====== منطق الدقيقة يعمل لكل الأوقات ويتكيّف مع المسافة ====== - // ... (الكود كما هو) ... - double effectivePerMin = _perMinuteByTime(currentTime, clubCtx); - int billableMinutes = totalMinutes; - if (billableDistance > longDistThresholdKm) { - effectivePerMin = longTripPerMin; - final int capped = - (billableMinutes > minuteCapLong) ? minuteCapLong : billableMinutes; - billableMinutes = capped - freeMinutesLong; - if (billableMinutes < 0) billableMinutes = 0; - } else if (billableDistance > mediumDistThresholdKm) { - effectivePerMin = longTripPerMin; - billableMinutes = (billableMinutes > minuteCapMedium) - ? minuteCapMedium - : billableMinutes; - } - - // ====== أسعار/كم قبل التخفيض ====== - // ... (الكود كما هو) ... - final double perKmComfortRaw = comfortPrice; - final double perKmDelivery = deliveryPrice; - final double perKmVanRaw = - (familyPrice > 0 ? familyPrice : (speedPrice + 13)); - final double perKmElectricRaw = perKmComfortRaw + electricPerKmUplift; - - // ====== تطبيق التخفيضات على الفئات (نفس نسبة Speed للبعيد) ====== - // ... (الكود كما هو) ... - double perKmComfort = perKmComfortRaw * (1.0 - distanceReduction); - double perKmElectric = perKmElectricRaw * (1.0 - distanceReduction); - double perKmVan = perKmVanRaw * (1.0 - distanceReduction); - perKmComfort = perKmComfort.clamp(0, double.infinity); - perKmElectric = perKmElectric.clamp(0, double.infinity); - perKmVan = perKmVan.clamp(0, double.infinity); - final double perKmBalash = (perKmSpeed - 5).clamp(0, double.infinity); - - // ====== دوال الاحتساب ====== - double _oneWayFare({ - required double perKm, - required bool isLady, - double flatAddon = 0, - }) { - double fare = billableDistance * perKm; - fare += - billableMinutes * effectivePerMin; // دقائق بعد السقف/العفو إن وُجد - fare += flatAddon; - if (isLady) fare += ladyFlatAddon; - if (airportCtx) fare += airportAddonSYP; - - // --- ⬇️ الإضافة الجديدة: تطبيق إضافة حدود المطار ⬇️ --- - if (damascusAirportBoundCtx || isInDamascusAirportBoundCtx) { - fare += damascusAirportBoundAddon; - } - // --- ⬆️ نهاية الإضافة ⬆️ --- - - return _applyMinFare(fare); - } - - double _roundTripFare({required double perKm}) { - // خصم 40% لمسافة إياب واحدة + زمن مضاعف (بنفس قواعد الدقيقة المعدّلة) - double distPart = - (billableDistance * 2 * perKm) - ((billableDistance * perKm) * 0.4); - double timePart = (billableMinutes * 2) * effectivePerMin; - double fare = distPart + timePart; - if (airportCtx) fare += airportAddonSYP; - - // --- ⬇️ الإضافة الجديدة: تطبيق إضافة حدود المطار ⬇️ --- - // تنطبق أيضاً على رحلات الذهاب والعودة لأنها "تصل" إلى الوجهة - if (damascusAirportBoundCtx || isInDamascusAirportBoundCtx) { - fare += damascusAirportBoundAddon; - } - // --- ⬆️ نهاية الإضافة ⬆️ --- - - return _applyMinFare(fare); - } - - // ====== حساب كل الفئات (Base قبل العمولة) ====== - final double costSpeed = _oneWayFare(perKm: perKmSpeed, isLady: false); - final double costBalash = _oneWayFare(perKm: perKmBalash, isLady: false); - final double costComfort = _oneWayFare(perKm: perKmComfort, isLady: false); - final double costElectric = _oneWayFare( - perKm: perKmElectric, isLady: false, flatAddon: electricFlatAddon); - final double costDelivery = - _oneWayFare(perKm: perKmDelivery, isLady: false); - final double costLady = _oneWayFare( - perKm: perKmComfort, - isLady: true); // Lady تعتمد Comfort بعد التخفيض + إضافة ثابتة - final double costVan = _oneWayFare(perKm: perKmVan, isLady: false); - final double costRayehGai = _roundTripFare(perKm: perKmSpeed); - final double costRayehGaiComfort = _roundTripFare(perKm: perKmComfort); - final double costRayehGaiBalash = _roundTripFare(perKm: perKmBalash); - - // ====== أسعار الراكب بعد العمولة (kazan من السيرفر) ====== - totalPassengerSpeed = _withCommission(costSpeed); - totalPassengerBalash = _withCommission(costBalash); - totalPassengerComfort = _withCommission(costComfort); - totalPassengerElectric = _withCommission(costElectric); - totalPassengerLady = _withCommission(costLady); - totalPassengerScooter = _withCommission(costDelivery); - totalPassengerVan = _withCommission(costVan); - totalPassengerRayehGai = _withCommission(costRayehGai); - totalPassengerRayehGaiComfort = _withCommission(costRayehGaiComfort); - totalPassengerRayehGaiBalash = _withCommission(costRayehGaiBalash); - - // افتراضي للعرض - totalPassenger = totalPassengerSpeed; - totalCostPassenger = totalPassenger; - - // ====== دعم رصيد محفظة سلبي ====== - try { - final walletStr = box.read(BoxName.passengerWalletTotal).toString(); - final walletVal = double.tryParse(walletStr) ?? 0.0; - if (walletVal < 0) { - final neg = (-1) * walletVal; - totalPassenger += neg; - totalPassengerComfort += neg; - totalPassengerElectric += neg; - totalPassengerLady += neg; - totalPassengerBalash += neg; - totalPassengerScooter += neg; - totalPassengerRayehGai += neg; - totalPassengerRayehGaiComfort += neg; - totalPassengerRayehGaiBalash += neg; - totalPassengerVan += neg; - } - } catch (e) { - Log.print("Error: $e"); - } - - update(); - changeBottomSheetShown(forceValue: true); - } - - List polylineCoordinate = []; - String? cardNumber; - void readyWayPoints() { - hintTextwayPointStringAll = [ - hintTextwayPoint0, - hintTextwayPoint1, - hintTextwayPoint2, - hintTextwayPoint3, - hintTextwayPoint4, - ]; - polylineCoordinatesPointsAll = [ - polylineCoordinates0, - polylineCoordinates1, - polylineCoordinates2, - polylineCoordinates3, - polylineCoordinates4, - ]; - allTextEditingPlaces = [ - wayPoint0Controller, - wayPoint1Controller, - wayPoint2Controller, - wayPoint3Controller, - wayPoint4Controller, - ]; - currentLocationToFormPlacesAll = [ - currentLocationToFormPlaces0, - currentLocationToFormPlaces1, - currentLocationToFormPlaces2, - currentLocationToFormPlaces3, - currentLocationToFormPlaces4, - ]; - placeListResponseAll = [ - wayPoint0, - wayPoint1, - wayPoint2, - wayPoint3, - wayPoint4 - ]; - startLocationFromMapAll = [ - startLocationFromMap0, - startLocationFromMap1, - startLocationFromMap2, - startLocationFromMap3, - startLocationFromMap4, - ]; - currentLocationStringAll = [ - currentLocationString0, - currentLocationString1, - currentLocationString2, - currentLocationString3, - currentLocationString4, - ]; - placesCoordinate = [ - placesCoordinate0, - placesCoordinate1, - placesCoordinate2, - placesCoordinate3, - placesCoordinate4, - ]; - update(); - } - - List driversForMishwari = []; - - Future selectDriverAndCarForMishwariTrip() async { - // Calculate the bounds for 12km range - double latitudeOffset = 0.1; // 20km range in latitude - double longitudeOffset = 0.12; // 20km range in longitude - - // Calculate bounding box based on passenger's location - double southwestLat = passengerLocation.latitude - latitudeOffset; - double northeastLat = passengerLocation.latitude + latitudeOffset; - double southwestLon = passengerLocation.longitude - longitudeOffset; - double northeastLon = passengerLocation.longitude + longitudeOffset; - - // Create the payload with calculated bounds - var payload = { - 'southwestLat': southwestLat.toString(), - 'northeastLat': northeastLat.toString(), - 'southwestLon': southwestLon.toString(), - 'northeastLon': northeastLon.toString(), - }; - - try { - // Fetch data from the API - var res = await CRUD().get( - link: AppLink.selectDriverAndCarForMishwariTrip, payload: payload); - - if (res != 'failure') { - // Check if response is valid JSON - try { - var d = jsonDecode(res); - driversForMishwari = d['message']; - Log.print('driversForMishwari: $driversForMishwari'); - update(); - } catch (e) { - // Handle invalid JSON format - Log.print("Error decoding JSON: $e"); - return 'Server returned invalid data. Please try again later.'; - } - } else { - return 'No driver available now, try again later. Thanks for using our app.' - .tr; - } - } catch (e) { - // Handle network or other exceptions - Log.print("Error fetching data: $e"); - return 'There was an issue connecting to the server. Please try again later.' - .tr; - } - } - - final Rx selectedDateTime = DateTime.now().obs; - - void updateDateTime(DateTime newDateTime) { - selectedDateTime.value = newDateTime; - } - - Future mishwariOption() async { - isLoading = true; - update(); - // add dialoug for select driver and car - await selectDriverAndCarForMishwariTrip(); - Future.delayed(Duration.zero); - isLoading = false; - update(); - Get.to(() => CupertinoDriverListWidget()); - - // changeCashConfirmPageShown(); - } - - var driverIdVip = ''; - Future saveTripData( - Map driver, DateTime tripDateTime) async { - try { - // Prepare trip data - Map tripData = { - 'id': driver['driver_id'].toString(), // Ensure the id is a string - 'phone': driver['phone'], - 'gender': driver['gender'], - 'name': driver['NAME'], - 'name_english': driver['name_english'], - 'address': driver['address'], - 'religion': driver['religion'] ?? 'UnKnown', - 'age': driver['age'].toString(), // Convert age to String - 'education': driver['education'] ?? 'UnKnown', //startlocationname - 'license_type': driver['license_type'] ?? 'UnKnown', - 'national_number': driver['national_number'] ?? 'UnKnown', - 'car_plate': driver['car_plate'], - 'make': driver['make'], - 'model': driver['model'], - 'year': driver['year'].toString(), // Convert year to String - 'color': driver['color'], - 'color_hex': driver['color_hex'], - 'displacement': driver['displacement'], - 'fuel': driver['fuel'], - 'token': driver['token'], - 'rating': driver['rating'].toString(), // Convert rating to String - 'countRide': - driver['ride_count'].toString(), // Convert countRide to String - 'passengerId': box.read(BoxName.passengerID), - 'timeSelected': tripDateTime.toIso8601String(), - 'status': 'pending', - 'startNameAddress': startNameAddress.toString(), - 'locationCoordinate': - '${data[0]["start_location"]['lat']},${data[0]["start_location"]['lng']}', - }; - Log.print('tripData: $tripData'); - - // Send data to server - var response = - await CRUD().post(link: AppLink.addMishwari, payload: tripData); - // Log.print('response: $response'); - - if (response != 'failure') { - // Trip saved successfully - // Get.snackbar('Success'.tr, 'Trip booked successfully'.tr); - var id = response['message']['id'].toString(); - await CRUD() - .post(link: '${AppLink.server}/ride/rides/add.php', payload: { - "start_location": - '${data[0]["start_location"]['lat']},${data[0]["start_location"]['lng']}', - "end_location": - '${data[0]["start_location"]['lat']},${data[0]["start_location"]['lng']}', - "date": DateTime.now().toString(), - "time": DateTime.now().toString(), - "endtime": DateTime.now().add(const Duration(hours: 2)).toString(), - "price": '50', - "passenger_id": box.read(BoxName.passengerID).toString(), - "driver_id": driver['driver_id'].toString(), - "status": "waiting", - 'carType': 'vip', - "price_for_driver": '50', - "price_for_passenger": '50', - "distance": '20', - "paymentMethod": 'cash', - }).then((value) { - if (value is String) { - final parsedValue = jsonDecode(value); - rideId = parsedValue['message']; - } else if (value is Map) { - rideId = value['message']; - } else { - Log.print('Unexpected response type: ${value.runtimeType}'); - } - }); - - driverIdVip = driver['driver_id'].toString(); - driverId = driver['driver_id'].toString(); - - DateTime timeSelected = DateTime.parse(tripDateTime.toIso8601String()); - Get.find().scheduleNotificationsForTimeSelected( - "Your trip is scheduled".tr, - "Don't forget your ride!".tr, - "tone1", - timeSelected); - - await NotificationService.sendNotification( - category: 'OrderVIP', - target: driver['token'].toString(), - title: 'OrderVIP'.tr, - body: '$rideId - VIP Trip', - isTopic: false, // Important: this is a token - tone: 'tone1', - driverList: [ - id, - rideId, - driver['id'], - passengerLocation.latitude.toString(), - startNameAddress.toString(), - passengerLocation.longitude.toString(), - (box.read(BoxName.name).toString().split(' ')[0]).toString(), - box.read(BoxName.passengerID).toString(), - box.read(BoxName.phone).toString(), - box.read(BoxName.email).toString(), - box.read(BoxName.passengerPhotoUrl).toString(), - box.read(BoxName.tokenFCM).toString(), - (driver['token'].toString()), - ], - ); - if (response['message'] == "Trip updated successfully") { - mySnackbarSuccess("Trip updated successfully".tr); - Log.print( - 'previous_driver_token: ${response['previous_driver_token']}'); - - await NotificationService.sendNotification( - category: 'Order VIP Canceld', - target: response['previous_driver_token'].toString(), - title: 'Order VIP Canceld'.tr, - body: 'Passenger cancel order'.tr, - isTopic: false, // Important: this is a token - tone: 'cancel', - driverList: [], - ); - } - // data = []; - isBottomSheetShown = false; - update(); - Get.to(() => VipWaittingPage()); - } else { - throw Exception('Failed to save trip'); - } - } catch (e) { - // Show error message with more details for debugging - Get.snackbar('Error'.tr, 'Failed to book trip: $e'.tr, - backgroundColor: AppColor.redColor); - Log.print('Error: $e'); - } - } - - Future cancelVip(String token, tripId) async { - var res = await CRUD() - .post(link: AppLink.cancelMishwari, payload: {'id': tripId}); - if (res != 'failur') { - Get.back(); - mySnackbarSuccess('You canceled VIP trip'.tr); - } - } - - void sendToDriverAgain(String token) { - NotificationService.sendNotification( - category: 'Order VIP Canceld', - target: token.toString(), - title: 'Order VIP Canceld'.tr, - body: 'Passenger cancel order'.tr, - isTopic: false, // Important: this is a token - tone: 'cancel', - driverList: [], - ); - } - -// دالة الفحص عند بدء التطبيق - Future detectAndCacheDeviceTier() async { - // 1. استخدام الكلاس الذي أنشأناه سابقاً للفحص - bool isHighEnd = await DevicePerformanceManager.isHighEndDevice(); - - // 2. طباعة النتيجة للتأكد - Log.print("Device Analysis - Is Flagship/HighEnd? $isHighEnd"); - - // 3. تخزين النتيجة بشكل منطقي صحيح - // إذا كان الجهاز قوياً (true)، فإن وضع الـ LowEnd يكون (false) - // والعكس صحيح - box.write(BoxName.lowEndMode, !isHighEnd); - } - - initilizeGetStorage() async { - if (box.read(BoxName.addWork) == null) { - box.write(BoxName.addWork, 'addWork'); - } - if (box.read(BoxName.addHome) == null) { - box.write(BoxName.addHome, 'addHome'); - } - if (box.read(BoxName.lowEndMode) == null) { - detectAndCacheDeviceTier(); - } - } - - late List recentPlaces = []; - - getFavioratePlaces() async { - recentPlaces = await sql.getCustomQuery( - 'SELECT * FROM ${TableName.recentLocations} ORDER BY createdAt DESC'); - // Log.print('recentPlaces: ${recentPlaces}'); - } - - double passengerRate = 5; - double comfortPrice = 45; - double speedPrice = 40; - double mashwariPrice = 40; - double familyPrice = 55; - double deliveryPrice = 1.2; - double minFareSYP = 16000; // حد أدنى للأجرة (سوريا) - double minBillableKm = 1.0; // حد أدنى للمسافة المفوترة - double commissionPct = 15; // عمولة التطبيق % (راكب) - - getKazanPercent() async { - var res = await CRUD().get( - link: AppLink.getKazanPercent, - payload: {'country': box.read(BoxName.countryCode).toString()}, - ); - if (res != 'failure') { - var json = jsonDecode(res); - // التحقق الديناميكي من 'data' أو 'message' - var dataList = json['data'] ?? json['message']; - - if (dataList != null && dataList is List && dataList.isNotEmpty) { - var firstRow = dataList[0]; - kazan = double.parse(firstRow['kazan'].toString()); - naturePrice = double.parse(firstRow['naturePrice'].toString()); - heavyPrice = double.parse(firstRow['heavyPrice'].toString()); - latePrice = double.parse(firstRow['latePrice'].toString()); - comfortPrice = double.parse(firstRow['comfortPrice'].toString()); - speedPrice = double.parse(firstRow['speedPrice'].toString()); - deliveryPrice = double.parse(firstRow['deliveryPrice'].toString()); - mashwariPrice = double.parse(firstRow['freePrice'].toString()); - familyPrice = double.parse(firstRow['familyPrice'].toString()); - fuelPrice = double.parse(firstRow['fuelPrice'].toString()); - } - } - } - - getPassengerRate() async { - var res = await CRUD().get( - link: AppLink.getPassengerRate, - payload: {'passenger_id': box.read(BoxName.passengerID)}); - if (res != 'failure') { - var json = jsonDecode(res); - var message = json['data'] ?? json['message']; - if (message['rating'] == null) { - passengerRate = 5.0; // Default rating - } else { - // Safely parse the rating to double - var rating = message['rating']; - if (rating is String) { - passengerRate = - double.tryParse(rating) ?? 5.0; // Default if parsing fails - } else if (rating is num) { - passengerRate = - rating.toDouble(); // Already a number, convert to double - } else { - passengerRate = 5.0; // Default for unexpected data types - } - } - } else { - passengerRate = 5.0; // Default rating for failure - } - } - - addFingerPrint() async { - String fingerPrint = await DeviceHelper.getDeviceFingerprint(); - await CRUD().postWallet(link: AppLink.addFingerPrint, payload: { - 'token': (box.read(BoxName.tokenFCM.toString())), - 'passengerID': box.read(BoxName.passengerID).toString(), - "fingerPrint": fingerPrint - }); - } - - firstTimeRunToGetCoupon() async { - // Check if it's the first time and the app is installed and gift token is available - if (box.read(BoxName.isFirstTime).toString() == '0' && - box.read(BoxName.isInstall).toString() == '1' && - box.read(BoxName.isGiftToken).toString() == '0') { - var promo, discount, validity; - var resPromo = await CRUD().get(link: AppLink.getPromoFirst, payload: { - "passengerID": box.read(BoxName.passengerID).toString(), - }); - if (resPromo != 'failure') { - var d1 = jsonDecode(resPromo); - promo = d1['message']['promo_code']; - discount = d1['message']['amount']; - validity = d1['message']['validity_end_date']; - } - box.write(BoxName.isFirstTime, '1'); - - // Show a full-screen modal styled as an ad - Get.dialog( - AlertDialog( - contentPadding: - EdgeInsets.zero, // Removes the padding around the content - content: SizedBox( - width: 300, // Match the width of PromoBanner - // height: 250, // Match the height of PromoBanner - child: PromoBanner( - promoCode: promo, - discountPercentage: discount, - validity: validity, - ), - ), - ), - ); - } - } - - // --- دالة جديدة للاستماع ومعالجة الرابط --- - void _listenForDeepLink() { - ever(_deepLinkController.rawDeepLink, (String? link) async { - if (link != null && link.isNotEmpty) { - Log.print('📍 MapPassengerController processing link: $link'); - - // 1. استخراج الإحداثيات باستخدام الدالة الموجودة لديك مسبقاً - Map? coordinates = - await extractCoordinatesFromLinkAsync(link); - - if (coordinates != null) { - double destLat = coordinates['latitude']!; - double destLng = coordinates['longitude']!; - myDestination = LatLng(destLat, destLng); - - // 2. التحقق من موقع الراكب الحالي - if (passengerLocation == null || - (passengerLocation.latitude == 0 && - passengerLocation.longitude == 0)) { - Log.print('⏳ Waiting for current location to calculate route...'); - await getLocation(); // جلب موقع الراكب إذا لم يكن متاحاً - } - - if (passengerLocation != null) { - String originStr = - '${passengerLocation.latitude},${passengerLocation.longitude}'; - String destStr = '$destLat,$destLng'; - - Log.print( - '🚀 Drawing route from Deep Link: $originStr to $destStr'); - - // 3. مسح أي مسارات ونقاط توقف سابقة - clearPolyline(); - waypoints.clear(); - clearAllMenuWaypoints(); - - // 4. استدعاء دالة رسم المسار وحساب التكلفة التي برمجتها - await getDirectionMap(originStr, destStr); - - // 5. إظهار الواجهة السفلية للرحلة ليكون الطلب جاهزاً بنقرة واحدة - isBottomSheetShown = true; - heightBottomSheetShown = 250; - update(); - - Get.snackbar( - 'Location Received'.tr, - 'Route and prices have been calculated successfully!'.tr, - backgroundColor: AppColor.greenColor, - colorText: Colors.white, - ); - } - } else { - Log.print('⚠️ Could not extract valid coordinates from link: $link'); - } - - // تفريغ الرابط بعد معالجته حتى لا يتم استدعاؤه مرة أخرى بالخطأ - _deepLinkController.rawDeepLink.value = null; - } - }); - - // معالجة الرابط إذا كان موجوداً مسبقاً (Cold Start) قبل تفعيل المستمع - if (_deepLinkController.rawDeepLink.value != null && - _deepLinkController.rawDeepLink.value!.isNotEmpty) { - String link = _deepLinkController.rawDeepLink.value!; - _deepLinkController.rawDeepLink.value = null; - - // نؤجل التنفيذ قليلاً لضمان تحميل الخريطة - Future.delayed(const Duration(milliseconds: 500), () async { - Log.print( - '📍 MapPassengerController processing link (Cold Start): $link'); - - Map? coordinates = - await extractCoordinatesFromLinkAsync(link); - - if (coordinates != null) { - double destLat = coordinates['latitude']!; - double destLng = coordinates['longitude']!; - myDestination = LatLng(destLat, destLng); - - if (passengerLocation == null || - (passengerLocation.latitude == 0 && - passengerLocation.longitude == 0)) { - await getLocation(); - } - - if (passengerLocation != null) { - String originStr = - '${passengerLocation.latitude},${passengerLocation.longitude}'; - String destStr = '$destLat,$destLng'; - - clearPolyline(); - waypoints.clear(); - clearAllMenuWaypoints(); - await getDirectionMap(originStr, destStr); - - isBottomSheetShown = true; - heightBottomSheetShown = 250; - update(); - } - } - }); - } - } - - @override - void onInit() async { - super.onInit(); - _checkAndRefreshMapStyle(); // Verify style version and clear cache if needed -// // --- إضافة جديدة: تهيئة وحدة التحكم في الروابط العميقة --- - Get.put(DeepLinkController(), permanent: true); -// // ---------------------------------------------------- - // مرحلة 0: الضروري جداً لعرض الخريطة سريعاً - // mapAPIKEY = await storage.read(key: BoxName.mapAPIKEY); - await initilizeGetStorage(); // إعداد سريع - await _initMinimalIcons(); // start/end فقط - // await addToken(); // لو لازم للمصادقة - _listenForDeepLink(); - // initSocket(); - await getLocation(); // لتحديد الكاميرا - box.write(BoxName.carType, 'yet'); - box.write(BoxName.tipPercentage, '0'); - // await detectAndCacheDeviceTier(); - - // لا تُنشئ Controllers الثقيلة الآن: - Get.lazyPut(() => TextToSpeechController(), - fenix: true); - Get.lazyPut(() => FirebaseMessagesController(), - fenix: true); - Get.lazyPut(() => AudioRecorderController(), - fenix: true); - - // ابدأ الخريطة الآن (الشاشة ظهرت للمستخدم) - Future.delayed(const Duration(seconds: 4), () { - if (isLoading) { - isLoading = false; - update(); - } - }); - - // مرحلة 1: مهام ضرورية للتسعير لكن غير حرجة لظهور UI - unawaited(_stagePricingAndState()); - - // مرحلة 2: تحسينات/كماليات بالخلفية - unawaited(_stageNiceToHave()); - - // ابدأ إعادة تحميل الماركر لكن بثروتل داخلي - // startMarkerReloading(); // تأكد أنه مَخنوق التحديث (throttled) - _startMasterTimer(); - - // Start listening to emergency shake gestures - EmergencySignalService.instance.startListening(() { - if (statusRide == 'Begin' || statusRide == 'start') { - Log.print("🚨 Emergency shake verified! Prompting SOS..."); - if (isBottomSheetShown) { - sosPassenger(); - } else { - Get.snackbar( - 'Emergency Mode Triggered'.tr, - 'Stay calm. We are here to help.'.tr, - backgroundColor: AppColor.redColor, - colorText: Colors.white, - duration: const Duration(seconds: 4), - ); - sosPassenger(); - } - } - }); - } - -// === Helpers === - - Future _initMinimalIcons() async { - // Icons are now loaded dynamically via MapLibre's _loadMapIcons onStyleLoaded - } - - Future _stagePricingAndState() async { - try { - await getKazanPercent(); // أسعار السيرفر - } catch (e) { - Log.print("Error: $e"); - } - try { - _checkInitialRideStatus(); // تحقق من حالة الرحلة الحالية - } catch (e) { - Log.print("Error: $e"); - } - // لو عندك ضبط “وضع خفيف” حسب الجهاز: - _applyLowEndModeIfNeeded(); - } - - Future _stageNiceToHave() async { - Log.print('🚀 MapPassengerController: Starting _stageNiceToHave'); - - // 🔥 Fix: Future.wait uses ONE argument (the list). - await Future.wait([ - Future(() async { - try { - Log.print('🔍 Loading Favorites...'); - getFavioratePlaces(); - } catch (e) { - Log.print("Error: $e"); - } - }), - Future(() async { - try { - Log.print('🔍 Loading Waypoints...'); - readyWayPoints(); - } catch (e) { - Log.print("Error: $e"); - } - }), - Future(() async { - try { - Log.print('🔍 Loading Rate...'); - getPassengerRate(); - } catch (e) { - Log.print("Error: $e"); - } - }), - Future(() async { - try { - Log.print('🔍 Loading Coupons...'); - firstTimeRunToGetCoupon(); - } catch (e) { - Log.print("Error: $e"); - } - }), - ]); - Log.print('✅ MapPassengerController: _stageNiceToHave complete'); - try { - cardNumber = await SecureStorage().readData(BoxName.cardNumber); - } catch (e) { - Log.print("Error: $e"); - } - } - - void _applyLowEndModeIfNeeded() { - // مثال بسيط: يمكنك حفظ فلاج بنظامك (من السيرفر/الإعدادات/الكاش) لتفعيل وضع خفيف - // لاحقاً فعّل: map.trafficEnabled=false, buildingsEnabled=false, تبسيط polylines... - // controller.lowEndMode = true; - } - - uploadPassengerLocation() async { - await CRUD().post(link: AppLink.addpassengerLocation, payload: { - "passengerId": box.read(BoxName.passengerID), - "lat": passengerLocation.latitude.toString(), - "lng": passengerLocation.longitude.toString(), - "rideId": rideId.toString() - }); - } - - void _showRideStartNotifications() { - // تنبيهات الأسعار حسب نوع السيارة - if (['Speed', 'Awfar Car'].contains(box.read(BoxName.carType))) { - NotificationController().showNotification('Fixed Price'.tr, - 'The captain is responsible for the route.'.tr, 'ding'); - } else if (['Comfort', 'Lady'].contains(box.read(BoxName.carType))) { - NotificationController().showNotification('Attention'.tr, - 'The price may increase if the route changes.'.tr, 'ding'); - } - } - - /// Checks the current version of assets/style.json and purges the map cache if it has changed. - Future _checkAndRefreshMapStyle() async { - try { - final String styleJson = await rootBundle.loadString('assets/style.json'); - final Map decoded = json.decode(styleJson); - final String? currentVersion = - decoded['metadata'] != null ? decoded['metadata']['version'] : null; - - if (currentVersion == null) return; - - final String lastVersion = box.read(BoxName.styleVersion) ?? "0.0.0"; - - if (currentVersion != lastVersion) { - Log.print( - "♻️ Map Style Version mismatch ($lastVersion -> $currentVersion). Purging offline cache..."); - await OfflineMapService.instance.clearCache(); - - // Final verification check: give native engine time to flush - await Future.delayed(const Duration(milliseconds: 500)); - - box.write(BoxName.styleVersion, currentVersion); - Log.print("✅ Style Version updated to $currentVersion"); - } - } catch (e) { - Log.print("⚠️ Style version check failed: $e"); - } - } -} - -class CarLocation { - final String id; - final double latitude; - final double longitude; - final double distance; - final double duration; - - CarLocation({ - required this.id, - required this.latitude, - required this.longitude, - this.distance = 10000, - this.duration = 10000, - }); -} +// import 'dart:async'; +// import 'package:Intaleq/services/offline_map_service.dart'; +// import 'package:Intaleq/services/emergency_signal_service.dart'; +// import 'package:Intaleq/views/widgets/mycircular.dart'; +// import 'dart:convert'; +// import 'dart:io'; +// import 'dart:math' show Random, atan2, cos, max, min, pi, pow, sin, sqrt; +// import 'dart:math' as math; +// import 'dart:ui'; +// import 'dart:typed_data'; +// import 'package:image/image.dart' as img; +// import 'package:Intaleq/services/ride_live_notification.dart'; +// import 'package:crypto/crypto.dart'; +// import 'package:Intaleq/views/Rate/rate_captain.dart'; +// import 'package:Intaleq/views/Rate/rating_driver_bottom.dart'; +// import 'package:device_info_plus/device_info_plus.dart'; +// import 'package:flutter/foundation.dart'; +// import 'package:flutter/services.dart'; +// import 'package:http/http.dart' as http; + +// import 'package:Intaleq/constant/univeries_polygon.dart'; +// import 'package:Intaleq/controller/firebase/local_notification.dart'; +// import 'package:flutter/cupertino.dart'; +// import 'package:flutter_confetti/flutter_confetti.dart' hide Circle; +// import 'package:socket_io_client/socket_io_client.dart' as IO; +// import 'package:vector_math/vector_math.dart' show radians; + +// import 'package:Intaleq/controller/functions/tts.dart'; +// import 'package:Intaleq/views/home/map_page_passenger.dart'; +// import 'package:Intaleq/views/widgets/my_textField.dart'; +// import 'package:flutter/material.dart'; +// import 'package:geolocator/geolocator.dart'; +// import 'package:get/get.dart'; +// import 'package:intaleq_maps/intaleq_maps.dart'; +// // import 'package:google_polyline_algorithm/google_polyline_algorithm.dart'; +// import 'package:intl/intl.dart'; +// import 'package:location/location.dart'; +// import 'package:Intaleq/constant/country_polygons.dart'; +// import 'package:Intaleq/constant/links.dart'; +// import 'package:Intaleq/constant/style.dart'; +// import 'package:Intaleq/controller/home/points_for_rider_controller.dart'; +// import 'package:Intaleq/views/home/map_widget.dart/form_serch_multiy_point.dart'; +// import '../../constant/api_key.dart'; +// import '../../constant/box_name.dart'; +// import '../../constant/colors.dart'; +// import '../../constant/info.dart'; +// import '../../constant/table_names.dart'; +// import '../../env/env.dart'; +// import '../../main.dart'; +// import '../../models/model/locations.dart'; +// import '../../models/model/painter_copoun.dart'; +// import '../../print.dart'; +// import '../../services/pip_service.dart'; +// import '../../views/home/map_widget.dart/cancel_raide_page.dart'; +// import '../../views/home/map_widget.dart/car_details_widget_to_go.dart'; +// import '../../views/home/map_widget.dart/select_driver_mishwari.dart'; +// import '../../views/widgets/elevated_btn.dart'; +// import '../../views/widgets/error_snakbar.dart'; +// import '../../views/widgets/mydialoug.dart'; +// import '../firebase/firbase_messge.dart'; +// import '../firebase/notification_service.dart'; +// import '../functions/audio_record1.dart'; +// import '../functions/crud.dart'; +// import '../functions/launch.dart'; +// import '../functions/package_info.dart'; +// import '../functions/secure_storage.dart'; +// import '../payment/payment_controller.dart'; +// import 'decode_polyline_isolate.dart'; +// import 'deep_link_controller.dart'; +// import 'device_performance.dart'; +// import 'ios_live_activity_service.dart'; +// import 'vip_waitting_page.dart'; + +// enum RideState { +// noRide, // لا يوجد رحلة جارية، عرض واجهة البحث +// cancelled, // تم إلغاء الرحلة +// preCheckReview, // يوجد رحلة منتهية، تحقق من التقييم +// searching, // جاري البحث عن كابتن +// driverApplied, // تم قبول الطلب +// driverArrived, // وصل السائق +// inProgress, // الرحلة بدأت بالفعل +// finished, // انتهت الرحلة (سيتم تحويلها إلى preCheckReview) +// } + +// class MapPassengerController extends GetxController { +// bool isLoading = true; +// TextEditingController placeDestinationController = TextEditingController(); +// TextEditingController increasFeeFromPassenger = TextEditingController(); +// TextEditingController placeStartController = TextEditingController(); +// TextEditingController wayPoint0Controller = TextEditingController(); +// TextEditingController wayPoint1Controller = TextEditingController(); +// TextEditingController wayPoint2Controller = TextEditingController(); +// TextEditingController wayPoint3Controller = TextEditingController(); +// TextEditingController wayPoint4Controller = TextEditingController(); +// TextEditingController sosPhonePassengerProfile = TextEditingController(); +// TextEditingController whatsAppLocationText = TextEditingController(); +// TextEditingController messageToDriver = TextEditingController(); +// final sosFormKey = GlobalKey(); +// final promoFormKey = GlobalKey(); +// final messagesFormKey = GlobalKey(); +// final increaseFeeFormKey = GlobalKey(); +// List data = []; +// List bounds = []; +// List placesStart = []; +// List driversToken = []; +// LatLng previousLocationOfDrivers = const LatLng(0, 0); +// double angleDegrees = 0; +// LatLng currentLocationOfDrivers = const LatLng(0, 0); +// List allTextEditingPlaces = []; +// List placesDestination = []; +// List wayPoint0 = []; +// List wayPoint1 = []; +// List wayPoint2 = []; +// List wayPoint3 = []; +// List wayPoint4 = []; +// final firebaseMessagesController = +// Get.isRegistered() +// ? Get.find() +// : Get.put(FirebaseMessagesController()); +// List> placeListResponseAll = []; + +// List placeListResponse = [ +// formSearchPlaces(0), +// formSearchPlaces(1), +// formSearchPlaces(2), +// formSearchPlaces(3), +// ]; + +// IntaleqMapController? mapController; +// bool isStyleLoaded = false; + +// Set markers = {}; +// Set polyLines = {}; +// Set polygons = {}; +// Set circles = {}; +// double speed = 0; +// PermissionStatus? permissionGranted; + +// LatLngBounds? lastComputedBounds; +// late LatLng passengerLocation = const LatLng(32, 34); +// late LatLng newMyLocation = const LatLng(32.115295, 36.064773); +// late LatLng newStartPointLocation = const LatLng(32.115295, 36.064773); +// late LatLng newPointLocation0 = const LatLng(32.115295, 36.064773); +// late LatLng newPointLocation1 = const LatLng(32.115295, 36.064773); +// late LatLng newPointLocation2 = const LatLng(32.115295, 36.064773); +// late LatLng newPointLocation3 = const LatLng(32.115295, 36.064773); +// late LatLng newPointLocation4 = const LatLng(32.115295, 36.064773); +// late LatLng myDestination; +// List polylineCoordinates = []; +// List polylineCoordinates0 = []; +// List polylineCoordinates1 = []; +// List polylineCoordinates2 = []; +// List polylineCoordinates3 = []; +// List polylineCoordinates4 = []; +// List> polylineCoordinatesPointsAll = []; +// List carsLocationByPassenger = []; +// List driverCarsLocationToPassengerAfterApplied = []; +// String markerIcon = "marker_icon"; +// String tripIcon = "trip_icon"; +// String startIcon = "start_icon"; +// String endIcon = "end_icon"; +// String carIcon = "car_icon"; +// String motoIcon = "moto_icon"; +// String ladyIcon = "lady_icon"; +// double height = 150; +// DateTime currentTime = DateTime.now(); +// final location = Location(); +// late LocationData currentLocation; +// double heightMenu = 0; +// double widthMenu = 0; +// double heightPickerContainer = 90; +// double heightPointsPageForRider = 0; +// double mainBottomMenuMapHeight = Get.height * .2; +// double wayPointSheetHeight = 0; +// String stringRemainingTimeToPassenger = ''; +// String stringRemainingTimeDriverWaitPassenger5Minute = ''; +// bool isDriverInPassengerWay = false; +// bool isDriverArrivePassenger = false; +// bool startLocationFromMap = false; +// bool isAnotherOreder = false; +// bool isWhatsAppOrder = false; +// bool passengerStartLocationFromMap = false; +// bool workLocationFromMap = false; +// bool homeLocationFromMap = false; +// bool isPassengerRideLocationWidget = false; +// bool startLocationFromMap0 = false; +// bool startLocationFromMap1 = false; +// bool startLocationFromMap2 = false; +// bool startLocationFromMap3 = false; +// bool startLocationFromMap4 = false; +// List startLocationFromMapAll = []; +// double latePrice = 0; +// double fuelPrice = 0; +// double heavyPrice = 0; +// double naturePrice = 0; +// bool heightMenuBool = false; +// String statusRide = 'wait'; +// String statusRideVip = 'wait'; +// bool statusRideFromStart = false; +// bool isPickerShown = false; +// bool isPointsPageForRider = false; +// bool isBottomSheetShown = false; +// bool mapType = false; +// bool reloadStartApp = false; +// bool mapTrafficON = false; +// bool isCancelRidePageShown = false; +// bool isCashConfirmPageShown = false; +// bool isPaymentMethodPageShown = false; +// bool isRideFinished = false; +// bool rideConfirm = false; +// bool isMarkersShown = false; +// bool isMainBottomMenuMap = true; + +// int durationToPassenger = 0; +// bool isWayPointSheet = false; +// bool isWayPointStopsSheet = false; +// bool isWayPointStopsSheetUtilGetMap = false; +// double heightBottomSheetShown = 0; +// double cashConfirmPageShown = 250; +// late String driverId = ''; +// late String gender = ''; +// double widthMapTypeAndTraffic = 50; +// double paymentPageShown = Get.height * .6; +// late LatLng southwest; +// late LatLng northeast; +// List carLocationsModels = []; +// var dataCarsLocationByPassenger; +// var datadriverCarsLocationToPassengerAfterApplied; +// CarLocation? nearestCar; + +// bool shouldFetch = true; // Flag to determine if fetch should be executed +// int selectedPassengerCount = 1; +// double progress = 0; +// double progressTimerToPassengerFromDriverAfterApplied = 0; +// double progressTimerDriverWaitPassenger5Minute = 0; +// int durationTimer = 9; +// int durationToRide = 0; +// int remainingTime = 25; +// int remainingTimeToPassengerFromDriverAfterApplied = 60; +// int remainingTimeDriverWaitPassenger5Minute = 60; +// int timeToPassengerFromDriverAfterApplied = 0; +// Timer? timerToPassengerFromDriverAfterApplied; +// bool rideTimerBegin = false; +// double progressTimerRideBegin = 0; +// int remainingTimeTimerRideBegin = 60; +// String stringRemainingTimeRideBegin = ''; +// String hintTextStartPoint = 'Search for your Start point'.tr; +// String hintTextwayPoint0 = 'Search for waypoint'.tr; +// String hintTextwayPoint1 = 'Search for waypoint'.tr; +// String hintTextwayPoint2 = 'Search for waypoint'.tr; +// String hintTextwayPoint3 = 'Search for waypoint'.tr; +// String hintTextwayPoint4 = 'Search for waypoint'.tr; +// String currentLocationString = 'Current Location'.tr; +// String currentLocationString0 = 'Current Location'.tr; +// String currentLocationString1 = 'Add Location 1'.tr; +// String currentLocationString2 = 'Add Location 2'.tr; +// String currentLocationString3 = 'Add Location 3'.tr; +// String currentLocationString4 = 'Add Location 4'.tr; +// String placesCoordinate0 = ''.tr; +// String placesCoordinate1 = ''.tr; +// String placesCoordinate2 = ''.tr; +// String placesCoordinate3 = ''.tr; +// String placesCoordinate4 = ''.tr; +// List currentLocationStringAll = []; +// List hintTextwayPointStringAll = []; +// var placesCoordinate = []; +// String hintTextDestinationPoint = 'Select your destination'.tr; +// late String rideId = 'yet'; +// bool noCarString = false; +// bool isCashSelectedBeforeConfirmRide = false; +// bool isPassengerChosen = false; +// bool isSearchingWindow = false; +// bool currentLocationToFormPlaces = false; +// bool currentLocationToFormPlaces0 = false; +// bool currentLocationToFormPlaces1 = false; +// bool currentLocationToFormPlaces2 = false; +// bool currentLocationToFormPlaces3 = false; +// bool currentLocationToFormPlaces4 = false; +// List currentLocationToFormPlacesAll = []; + +// // ── Multi-Waypoint (max 2 stops) ────────────────────────────────────────── +// List menuWaypoints = [null, null]; +// List menuWaypointNames = ['', '']; +// int activeMenuWaypointCount = 0; +// bool isPickingWaypoint = false; +// int pickingWaypointIndex = -1; + +// late String driverToken = ''; +// int carsOrder = 0; +// int wayPointIndex = 0; +// late double kazan = 8; +// String? mapAPIKEY; +// late double totalME = 0; +// late double tax = 0; +// late double totalPassenger = 0; +// late double totalCostPassenger = 0; +// late double totalPassengerComfort = 0; +// late double totalPassengerComfortDiscount = 0; +// late double totalPassengerElectricDiscount = 0; +// late double totalPassengerLadyDiscount = 0; +// late double totalPassengerSpeedDiscount = 0; +// late double totalPassengerBalashDiscount = 0; +// late double totalPassengerRaihGaiDiscount = 0; +// late double totalPassengerScooter = 0; +// late double totalPassengerVan = 0; +// late double totalDriver = 0; +// late double averageDuration = 0; +// late double costDuration = 0; +// late double costDistance = 0; +// late double distance = 0; +// late double duration = 0; +// bool _isDriverAppliedLogicExecuted = false; // فلاج لمنع التنفيذ المتكرر +// bool _isDriverArrivedLogicExecuted = false; +// bool _isRideBeginLogicExecuted = false; +// DateTime? _searchStartTime; // لتتبع مدة البحث +// DateTime? _lastDriversNotifyTime; // لتتبع آخر مرة تم إرسال إشعار للسائقين +// final int _masterTimerIntervalSeconds = 5; // فاصل زمني ثابت للمؤقت الرئيسي +// final int _searchTimeoutSeconds = 60; // مهلة البحث قبل عرض خيار زيادة السعر +// final int _notifyDriversIntervalSeconds = +// 25; // إرسال إشعار للسائقين كل 25 ثانية +// // متغير لمنع أي عمليات تحديث أثناء التقييم +// bool _isRatingScreenOpen = false; +// // --- إضافة جديدة: متغيرات لإدارة البحث المتوسع --- +// int _currentSearchPhase = 0; // لتتبع المرحلة الحالية للبحث +// bool _isFetchingDriverLocation = false; // متغير لمنع تكرار الطلب + +// // === استبدل initSocket بالكامل === +// late IO.Socket socket; +// bool isSocketConnected = false; +// int _reconnectAttempts = 0; +// final int _maxReconnectAttempts = 5; +// Timer? _reconnectTimer; +// var currentRideId; +// // لتخزين نقاط مسار السائق الحالية للمقارنة +// List _currentDriverRoutePoints = []; + +// // متغير لتتبع مصدر القبول — Socket أم غيره +// String _rideAcceptedViaSource = "Unknown"; +// // عدّاد تحديثات الموقع المستلمة من السوكيت (لقياس الصحة) +// int _socketLocationUpdatesCount = 0; + +// final Map _pollingIntervals = { +// RideState.noRide: 6, +// RideState.searching: 8, +// RideState.driverApplied: 10, +// RideState.driverArrived: 15, +// RideState.inProgress: 15, +// RideState.cancelled: 3600, +// RideState.finished: 3600, +// RideState.preCheckReview: 3600, +// }; +// // لمنع التكرار (عشان ما يعمل 100 طلب في نفس اللحظة) +// bool _isRecalculatingRoute = false; +// // متغير لمراقبة صحة السوكيت +// DateTime? _lastSocketLocationTime; +// // مسافة السماحية (مثلاً 150 متر) قبل اعتبار السائق "خارج المسار" +// final double _deviationThresholdMeters = 150.0; +// // ... (باقي الـ Imports) + +// // متغيرات التحكم +// Timer? _locationPollingTimer; // تايمر مخصص للموقع فقط + +// // ============================================================================== +// // 1. الدالة الرئيسية لتأسيس الاتصال (تستدعى عند بدء البحث startSearchingForDriver) +// // ============================================================================== +// Timer? _heartbeatTimer; +// void initConnectionWithSocket() { +// if (isSocketConnected && socket != null) return; + +// String passengerId = box.read(BoxName.passengerID).toString(); +// Log.print("🔌 Initializing Socket for Passenger: $passengerId"); + +// socket = IO.io( +// AppLink.serverSocket, +// IO.OptionBuilder() +// .setTransports(['websocket']) +// .disableAutoConnect() +// .setQuery({'id': passengerId}) +// // ✅ [FIX] إعادة اتصال شبه-لانهائية (999 محاولة) بدلاً من 20 +// .setReconnectionAttempts(20) +// // ✅ [FIX] تأخير أقل (1.5 ثانية) مع حد أقصى (8 ثواني) للتسريع +// .setReconnectionDelay(1500) +// .setReconnectionDelayMax(8000) +// .enableReconnection() +// .setExtraHeaders({'Connection': 'Upgrade'}) +// .build(), +// ); + +// socket.connect(); + +// // ✅ معالج الاتصال الأول +// socket.onConnect((_) { +// Log.print("✅ Socket Connected Successfully"); +// isSocketConnected = true; +// _reconnectAttempts = 0; +// _startHeartbeat(); + +// // ✅ [FIX] الاشتراك مجدداً في أحداث الرحلة عند كل اتصال +// if (rideId != null && rideId != 'yet' && driverId.isNotEmpty) { +// socket.emit('subscribe_driver_location', { +// 'ride_id': rideId, +// 'driver_id': driverId, +// }); +// Log.print("📡 Re-subscribed to driver location after connect"); +// } + +// update(); +// }); + +// // ⚠️ معالج الانقطاع +// socket.onDisconnect((_) { +// Log.print("⚠️ Socket Disconnected — Auto-Reconnect will handle it"); +// isSocketConnected = false; + +// // تفعيل Polling أسرع كـ Fallback مؤقت (سيتم إيقافه عند عودة الاتصال) +// if (_isActiveRideState()) { +// Log.print("🔄 Enabling Fast Polling Fallback (4s) until reconnect..."); +// _startMasterTimerWithInterval(4); +// } +// update(); +// }); + +// // 🔁 [FIX] معالج إعادة الاتصال الناجحة +// socket.onReconnect((_) { +// Log.print("🔁 Socket Reconnected Successfully!"); +// isSocketConnected = true; +// _reconnectAttempts = 0; + +// // استئناف النبضة فوراً +// _startHeartbeat(); + +// // إعادة الاشتراك في أحداث الرحلة +// if (rideId != null && rideId != 'yet' && driverId.isNotEmpty) { +// socket.emit('subscribe_driver_location', { +// 'ride_id': rideId, +// 'driver_id': driverId, +// }); +// Log.print("📡 Re-subscribed to driver location after reconnect"); +// } + +// // ✅ [FIX] إيقاف الـ Fast Polling لأن السوكيت عاد +// if (_isActiveRideState()) { +// Log.print("✅ Socket back online — stopping Fast Polling Fallback"); +// _masterTimer?.cancel(); +// _masterTimer = null; +// } + +// update(); +// }); + +// // 🔄 [FIX] معالج محاولات إعادة الاتصال (للتشخيص) +// socket.onReconnectAttempt((attemptNumber) { +// Log.print("🔄 Socket Reconnect Attempt #$attemptNumber..."); +// }); + +// // ❌ معالج الأخطاء +// socket.onError((error) { +// Log.print("❌ Socket Error: $error"); +// isSocketConnected = false; +// }); + +// // 📩 معالج تحديثات الحالة +// socket.on('ride_status_change', (data) { +// Log.print("📩 Socket Event: ride_status_change -> $data"); +// _handleRideStatusChangeWithSocket(data); +// }); + +// // 📍 معالج موقع السائق +// socket.on('driver_location_update', (data) { +// handleDriverLocationUpdate(data); +// }); +// } + +// void _startHeartbeat() { +// _heartbeatTimer?.cancel(); +// _heartbeatTimer = Timer.periodic(const Duration(seconds: 25), (timer) { +// if (isSocketConnected && socket.connected) { +// socket.emit('heartbeat', +// {'passenger_id': box.read(BoxName.passengerID).toString()}); +// } +// }); +// } + +// // دالة مساعدة +// bool _isActiveRideState() { +// return currentRideState.value == RideState.searching || +// currentRideState.value == RideState.driverApplied || +// currentRideState.value == RideState.driverArrived || +// currentRideState.value == RideState.inProgress; +// } + +// /// فحص سريع: هل السوكيت يعمل ويرسل بيانات؟ +// bool _isSocketHealthy() { +// if (!isSocketConnected) return false; +// if (_lastSocketLocationTime == null) return false; +// final diff = DateTime.now().difference(_lastSocketLocationTime!).inSeconds; +// return diff < 20; // إذا آخر تحديث قبل أقل من 20 ثانية +// } + +// /// 🧠 خوارزمية ذكية: حساب أقصر مسافة بين موقع السائق والـ Polyline (بدون API) +// double _calculateDistanceToPolyline(LatLng point, List polyline) { +// if (polyline.isEmpty) return 999.0; +// double minDistance = double.infinity; + +// for (int i = 0; i < polyline.length - 1; i++) { +// double d = _distToSegment(point, polyline[i], polyline[i + 1]); +// if (d < minDistance) minDistance = d; +// } +// return minDistance; +// } + +// double _distToSegment(LatLng p, LatLng v, LatLng w) { +// double l2 = _dist2(v, w); +// if (l2 == 0) return _distanceBetween(p, v); +// double t = ((p.latitude - v.latitude) * (w.latitude - v.latitude) + +// (p.longitude - v.longitude) * (w.longitude - v.longitude)) / +// l2; +// t = max(0, min(1, t)); +// return _distanceBetween( +// p, +// LatLng(v.latitude + t * (w.latitude - v.latitude), +// v.longitude + t * (w.longitude - v.longitude))); +// } + +// double _dist2(LatLng v, LatLng w) { +// return pow(v.latitude - w.latitude, 2).toDouble() + +// pow(v.longitude - w.longitude, 2).toDouble(); +// } + +// double _distanceBetween(LatLng p1, LatLng p2) { +// return Geolocator.distanceBetween( +// p1.latitude, p1.longitude, p2.latitude, p2.longitude); +// } + +// // ============================================================================== +// // 2. العقل المدبر: توجيه الحالات +// // ============================================================================== +// void _handleRideStatusChangeWithSocket(dynamic data) { +// if (data == null || data['status'] == null) return; + +// String newStatus = data['status'].toString().toLowerCase(); +// Log.print("🔔 Socket Status Update: $newStatus"); +// // استخراج بيانات السائق إذا توفرت (تأتي من acceptRide.php) +// Map? driverInfo; +// if (data['driver_info'] != null && data['driver_info'] is Map) { +// driverInfo = Map.from(data['driver_info']); +// } +// switch (newStatus) { +// case 'accepted': // أو apply/applied حسب تسمية السيرفر +// _onDriverAcceptedWithSocket(data, driverData: driverInfo); +// break; + +// case 'arrived': +// _onDriverArrivedWithSocket(); +// break; + +// case 'started': // أو begin +// _onRideStartedWithSocket(); +// break; + +// case 'finished': // أو ended +// _onRideFinishedWithSocket(data); +// break; + +// case 'cancelled': +// _onRideCancelledWithSocket(data); +// break; + +// case 'no_drivers_found': +// showNoDriverDialog(); +// break; +// } +// } + +// // ============================================================================== +// // 3. دوال المعالجة التفصيلية (Actions) +// // ============================================================================== +// void showNoDriverDialog() { +// Get.defaultDialog( +// title: "No Drivers Found".tr, +// middleText: +// "Sorry, there are no cars available of this type right now.".tr, +// textConfirm: "Refresh Map".tr, +// textCancel: "Cancel".tr, +// confirmTextColor: Colors.white, +// onConfirm: () { +// Get.back(); // إغلاق الديالوج +// restCounter(); +// stopAllTimers(); +// Get.offAll(() => MapPagePassenger()); // إعادة تحميل صفحة الخريطة +// }, +// ); +// } + +// // أ) عند قبول السائق للرحلة +// // أ) عند قبول السائق للرحلة (معدلة) +// // دالة الاستقبال من السوكيت (تصبح مجرد محول) +// void _onDriverAcceptedWithSocket(dynamic data, +// {Map? driverData}) { +// // استخراج البيانات وتمريرها للدالة الموحدة +// Map? info = driverData; + +// // دعم الهيكلية الجديدة +// if (info == null && data['driver_info'] != null) { +// info = Map.from(data['driver_info']); +// } +// // دعم الهيكلية القديمة (إن وجدت) +// else if (info == null && data['driverList'] != null) { +// // تحويل driverList إلى map إذا لزم الأمر +// } + +// processRideAcceptance(driverData: info, source: "Socket"); +// } + +// void _fillDriverDataLocally(Map data) { +// try { +// // تعبئة المتغيرات بناءً على أسماء الحقول في acceptRide.php +// driverId = data['driver_id']?.toString() ?? ''; +// driverPhone = data['phone']?.toString() ?? ''; + +// String fName = data['first_name']?.toString() ?? ''; +// String lName = data['last_name']?.toString() ?? ''; +// passengerName = lName.isNotEmpty +// ? "$fName $lName" +// : fName; // (هنا المتغير اسمه passengerName لكنه يحمل اسم السائق في الكود لديك) +// driverName = passengerName; + +// make = data['make']?.toString() ?? ''; +// model = data['model']?.toString() ?? ''; +// carColor = data['color']?.toString() ?? ''; +// colorHex = data['color_hex']?.toString() ?? ''; +// licensePlate = data['car_plate']?.toString() ?? ''; +// carYear = data['year']?.toString() ?? ''; + +// driverRate = data['ratingDriver']?.toString() ?? '5.0'; +// driverToken = data['token']?.toString() ?? ''; + +// // إذا كان هناك أي بيانات أخرى تحتاجها الواجهة +// update(); +// } catch (e) { +// Log.print("Error parsing socket driver data: $e"); +// } +// } + +// // دالة موحدة: تجلب المسار + الوقت + المسافة + ترسم الخط + تضبط الكاميرا +// Future calculateDriverToPassengerRoute( +// LatLng driverPos, LatLng passengerPos) async { +// // 1. تجهيز الرابط (نفس API الـ Direction) +// // نستخدم overview=full للحصول على الرسمة، و steps=false لتخفيف البيانات +// final Map queryParams = { +// 'fromLat': driverPos.latitude.toString(), +// 'fromLng': driverPos.longitude.toString(), +// 'toLat': passengerPos.latitude.toString(), +// 'toLng': passengerPos.longitude.toString(), +// }; +// final uri = +// Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams); + +// Log.print('📍 Calculating Driver Route: $uri'); + +// try { +// final response = await http.get(uri, headers: { +// 'x-api-key': Env.mapSaasKey, +// }).timeout(const Duration(seconds: 20)); + +// if (response.statusCode == 200) { +// final responseData = json.decode(response.body); + +// // Support both old format (routes[0]) and new SaaS format (top-level) +// var routeData = responseData['routes'] != null +// ? responseData['routes'][0] +// : responseData; + +// // 2. تحديث المتغيرات (المسافة والوقت) +// double durationSecondsRaw = (routeData['duration'] as num).toDouble(); +// int finalDurationSeconds = +// (durationSecondsRaw * kDurationScalar).toInt(); +// double distanceMeters = (routeData['distance'] as num).toDouble(); + +// timeToPassengerFromDriverAfterApplied = finalDurationSeconds; +// remainingTimeToPassengerFromDriverAfterApplied = finalDurationSeconds; +// distanceByPassenger = distanceMeters.toStringAsFixed(0); + +// // تحديث نصوص الواجهة +// int minutes = (finalDurationSeconds / 60).floor(); +// int seconds = finalDurationSeconds % 60; +// stringRemainingTimeToPassenger = +// '$minutes:${seconds.toString().padLeft(2, '0')}'; + +// Log.print( +// '✅ Driver Route Info: $minutes min, ${distanceMeters.toInt()} m'); + +// // 3. معالجة الرسم (Polyline) +// // SaaS uses 'points', OSRM uses 'geometry' +// String pointsString = +// routeData['points'] ?? routeData['geometry'] ?? ""; +// if (pointsString.isNotEmpty) { +// List decodedPoints = +// await compute(decodePolylineIsolate, pointsString); +// // حفظ نسخة للمقارنة +// _currentDriverRoutePoints = decodedPoints; +// // إزالة خط مسار السائق القديم فقط +// polyLines = polyLines +// .where((p) => p.polylineId.value != 'driver_route') +// .toSet(); + +// // إضافة الخط الجديد (بستايل مميز للسائق) +// polyLines = { +// ...polyLines, +// Polyline( +// polylineId: const PolylineId('driver_route'), +// points: decodedPoints, +// color: +// const Color(0xFF333333), // لون مختلف عن مسار الرحلة الأساسي +// width: 5, +// ) +// }; +// } + +// // 4. ضبط الكاميرا لتشمل السائق والراكب +// _fitCameraToPoints(driverPos, passengerPos); + +// update(); // تحديث واحد للكل +// } +// } catch (e) { +// Log.print('❌ Error calculating driver route: $e'); +// } +// } + +// Future _checkAndRecalculateIfDeviated(LatLng driverPos) async { +// // 1. شروط الخروج السريع +// if (_isRecalculatingRoute || _currentDriverRoutePoints.isEmpty) return; + +// // 2. حساب المسافة لأقرب نقطة في المسار (خوارزمية سريعة) +// // نستخدم مكتبة Geolocator أو حساب رياضي بسيط +// double minDistance = 100000.0; + +// // لتقليل الحمل، لا نفحص كل النقاط، نفحص عينة (كل 5 نقاط مثلاً) أو الكل إذا المسار قصير +// for (var point in _currentDriverRoutePoints) { +// double dist = Geolocator.distanceBetween(driverPos.latitude, +// driverPos.longitude, point.latitude, point.longitude); +// if (dist < minDistance) minDistance = dist; +// } + +// // 3. اتخاذ القرار +// if (minDistance > _deviationThresholdMeters) { +// Log.print("⚠️ Driver deviated ($minDistance m). Recalculating route..."); + +// _isRecalculatingRoute = true; + +// // إعادة حساب المسار من موقع السائق الجديد +// await calculateDriverToPassengerRoute(driverPos, passengerLocation); + +// _isRecalculatingRoute = false; +// } +// } + +// // ب) عند وصول السائق +// void _onDriverArrivedWithSocket() { +// Log.print("🚖 Driver Arrived (Socket)"); + +// processDriverArrival("Socket"); +// } + +// // ج) عند بدء الرحلة +// void _onRideStartedWithSocket() { +// Log.print("🚀 Ride Started (Socket)"); +// processRideBegin(source: "Socket"); +// } + +// // ربط السوكيت +// // د) عند انتهاء الرحلة (Socket Listener) +// void _onRideFinishedWithSocket(dynamic data) { +// Log.print("🏁 Ride Finished (Socket)"); + +// // نحاول استخراج DriverList من البيلود القادم من PHP +// // في finish_ride_updates.php أسميناه 'DriverList' +// var rawList = data['DriverList']; + +// List listToSend = []; + +// if (rawList != null) { +// if (rawList is List) { +// listToSend = rawList; +// } else if (rawList is String) { +// // احتياطاً لو وصل كنص +// try { +// listToSend = jsonDecode(rawList); +// } catch (e) { +// Log.print("Error: $e"); +// } +// } +// } + +// // إذا كانت القائمة فارغة، نحاول بناءها من البيانات المتفرقة (Fallback) +// if (listToSend.isEmpty && data['price'] != null) { +// listToSend = [ +// driverId, // 0 +// rideId, // 1 +// driverToken, // 2 +// data['price'].toString() // 3 +// ]; +// } + +// // استدعاء المعالج الموحد +// processRideFinished(listToSend, source: "Socket"); +// } + +// // هـ) عند الإلغاء +// void _onRideCancelledWithSocket(dynamic data) { +// processRideCancelledByDriver(data, source: "Socket"); +// } + +// // ============================================================================== +// // 4. إدارة تتبع الموقع (Polling) - مفصولة عن السوكيت +// // ============================================================================== +// // متغير لمنع التكرار (Race Condition Guard) +// bool _isCancelProcessed = false; + +// /// **معالجة إلغاء الرحلة الموحدة (Gatekeeper)** +// /// +// /// تستدعى من [Socket] أو [FCM] عند قيام السائق بإلغاء الرحلة. +// /// تضمن عدم تضارب الإشعارات وتوحد تجربة المستخدم. +// Future processRideCancelledByDriver(dynamic data, +// {String source = "Unknown"}) async { +// if (_isCancelProcessed) return; + +// _isCancelProcessed = true; +// stopAllTimers(); +// if (Get.isDialogOpen == true) Get.back(); +// await RideLiveNotification.cancel(); +// IosLiveActivityService.endRideActivity(); // ✅ أضف هذا السطر +// PipService.disablePip(); // ✅ إيقاف PiP عند انتهاء الرحلة +// if (Get.isDialogOpen == true) Get.back(); +// await RideLiveNotification.cancel(); +// Get.defaultDialog( +// title: "Sorry 😔".tr, // استخدام المفتاح الإنجليزي +// titleStyle: +// const TextStyle(color: Colors.red, fontWeight: FontWeight.bold), +// barrierDismissible: false, +// content: Column( +// children: [ +// const Icon(Icons.cancel_presentation, +// size: 50, color: Colors.redAccent), +// const SizedBox(height: 10), +// Text( +// "The driver cancelled the trip for an emergency reason.\nDo you want to search for another driver immediately?" +// .tr, +// textAlign: TextAlign.center, +// ), +// ], +// ), +// actions: [ +// TextButton( +// onPressed: () { +// Get.back(); +// handleNoDriverFound(); +// }, +// child: Text("Cancel Trip".tr, +// style: const TextStyle(color: Colors.grey)), +// ), +// ElevatedButton.icon( +// style: +// ElevatedButton.styleFrom(backgroundColor: AppColor.primaryColor), +// icon: const Icon(Icons.refresh, color: Colors.white), +// label: Text("Search for another driver".tr, +// style: const TextStyle(color: Colors.white)), +// onPressed: () { +// Get.back(); +// retrySearchForDrivers(); +// }, +// ), +// ], +// ); +// } + +// Future handleNoDriverFound() async { +// stopAllTimers(); +// await RideLiveNotification.cancel(); +// IosLiveActivityService.endRideActivity(); // ✅ أضف هذا السطر +// PipService.disablePip(); // ✅ إيقاف PiP +// _isCancelProcessed = false; +// currentRideState.value = RideState.noRide; +// resetAllMapStates(); +// Get.offAll(() => const MapPagePassenger()); + +// Get.defaultDialog( +// title: "We apologize 😔".tr, +// middleText: "No drivers found at the moment.\nPlease try again later.".tr, +// confirm: ElevatedButton( +// onPressed: () => Navigator.pop(Get.context!), +// child: Text("Ok".tr), +// ), +// ); +// } + +// /// **إعادة البحث عن سائقين (Retry Search)** +// /// +// /// تقوم باستدعاء السكربت لإعادة تفعيل الرحلة وبدء عداد البحث من جديد. +// void retrySearchForDrivers() async { +// _isCancelProcessed = false; +// isSearchingWindow = true; +// currentRideState.value = RideState.searching; +// driversStatusForSearchWindow = 'Searching for nearby drivers...'.tr; +// update(); + +// try { +// Log.print("🔄 Retrying search for ride ID: $rideId"); + +// // تجهيز البيانات المخزنة للإرسال +// var payload = { +// "ride_id": rideId.toString(), +// "passenger_id": box.read(BoxName.passengerID).toString(), +// "passenger_name": box.read(BoxName.name).toString(), +// "passenger_phone": box.read(BoxName.phone).toString(), +// "passenger_email": box.read(BoxName.email).toString(), +// "passenger_token": box.read(BoxName.tokenFCM).toString(), +// "passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(), +// "passenger_rating": "5.0", // أو قراءة التقييم الحقيقي إن وجد + +// // قراءة البيانات من المتغيرات المحفوظة في الكنترولر أو الـ Box +// "start_lat": startLocation.latitude.toString(), +// "start_lng": startLocation.longitude.toString(), +// "end_lat": endLocation.latitude.toString(), +// "end_lng": endLocation.longitude.toString(), +// "start_name": startNameAddress, +// "end_name": endNameAddress, +// "distance": distance.toString(), +// "distance_text": distanceByPassenger ?? "", +// "duration_text": durationToPassenger.toString(), +// "price": totalPassenger.toString(), +// "price_for_driver": costForDriver.toString(), +// "car_type": box.read(BoxName.carType).toString(), +// "is_wallet": Get.find().isWalletChecked.toString(), + +// // الخطوات (اختياري) +// "has_steps": Get.find().wayPoints.length > 1 +// ? "true" +// : "false", +// // يمكنك إضافة الخطوات إذا كانت لديك في مصفوفة +// }; + +// var response = await CRUD().post( +// link: "${AppLink.rideServerSide}/rides/retry_search_drivers.php", +// payload: payload, +// ); + +// if (response['status'] == 'success') { +// Log.print("✅ Search reset successfully."); +// startSearchingTimer(); +// } else { +// mySnackbarWarning("Failed to search, please try again later".tr); +// handleNoDriverFound(); +// } +// } catch (e) { +// Log.print("Error retrying search: $e"); +// mySnackbarWarning("Please check your internet connection".tr); +// handleNoDriverFound(); +// } +// } + +// Timer? _searchTimer; + +// /// **بدء مؤقت البحث (Search Timer)** +// /// +// /// يبدأ عداداً (مثلاً 90 ثانية). إذا لم يتم قبول الرحلة خلال هذه المدة، +// /// يتم إنهاء البحث واستدعاء [handleNoDriverFound]. +// Future startSearchingTimer() async { +// _searchTimer?.cancel(); +// int seconds = 0; + +// Log.print("⏳ Search Timer Started (90s)..."); +// await RideLiveNotification.showSearching(driversStatusForSearchWindow); + +// _searchTimer = Timer.periodic(const Duration(seconds: 1), (timer) { +// seconds++; + +// // إذا تغيرت الحالة (مثلاً سائق قبل الرحلة)، نوقف العداد +// if (currentRideState.value != RideState.searching) { +// timer.cancel(); +// return; +// } + +// // انتهاء الوقت (90 ثانية) +// if (seconds >= 90) { +// timer.cancel(); +// handleNoDriverFound(); // ⏳ انتهى الوقت ولم نجد سائقاً +// } +// }); +// } + +// // متغير لمنع التكرار (Race Condition Guard) +// bool _isArrivalProcessed = false; + +// /// **معالجة وصول السائق الموحدة (Unified Driver Arrival Handler)** +// /// +// /// تقوم هذه الدالة بإدارة حدث وصول السائق إلى موقع الراكب، وتعمل كـ "حارس بوابة" (Gatekeeper) +// /// لتوحيد المصادر سواء كانت من **WebSocket** أو **Firebase Notification**. +// /// +// /// **المهام التي تقوم بها:** +// /// 1. **منع التضارب (Race Condition Guard):** تتأكد أن الحدث لم يتم تنفيذه مسبقاً لتجنب تكرار الإشعارات أو إعادة ضبط العدادات. +// /// 2. **تحديث الحالة:** تغير حالة الرحلة فوراً إلى [RideState.driverArrived]. +// /// 3. **تفعيل الواجهة:** تظهر ديالوج "السائق وصل" وتبدأ عداد الانتظار المجاني (5 دقائق). +// /// +// /// * [source] : نص يوضح مصدر الاستدعاء (مثل "Socket" أو "FCM") لأغراض التتبع (Logging). +// Future processDriverArrival(String source) async { +// // 1. الحارس: إذا تم التنفيذ سابقاً، توقف +// if (currentRideState.value == RideState.driverArrived || +// _isArrivalProcessed) { +// Log.print("✋ Ignored Arrival from $source. Already processed."); +// return; +// } + +// _isArrivalProcessed = true; +// Log.print("🚖 Driver Arrived via $source! Processing..."); + +// // 2. تحديث الحالة +// currentRideState.value = RideState.driverArrived; +// statusRide = 'Arrived'; +// await RideLiveNotification.showDriverArrived(driverName ?? ''); + +// // 3. تشغيل واجهة الوصول والعداد +// driverArrivePassengerDialoge(); +// startTimerDriverWaitPassenger5Minute(); + +// // 4. إزالة مسار السائق واستعادة مسار الراكب للوجهة +// if (polylineCoordinates.isNotEmpty) { +// _playRouteAnimation(polylineCoordinates, lastComputedBounds); +// } + +// update(); +// } + +// // متغير لمنع التكرار +// bool _isFinishProcessed = false; + +// /// **معالجة إنهاء الرحلة الموحدة (Unified Ride Finish Handler)** +// /// +// /// تستدعى عند استلام حدث النهاية من السوكيت أو FCM. +// /// تقوم بإغلاق السوكيت، إيقاف التتبع، والانتقال لشاشة التقييم والدفع. +// /// +// /// * [driverList]: قائمة البيانات [driverId, rideId, token, price]. +// Future processRideFinished(List driverList, +// {String source = "Unknown"}) async { +// // 1. الحارس: منع التكرار +// if (currentRideState.value == RideState.finished || _isFinishProcessed) { +// Log.print("✋ Ignored Finish Request from $source. Already Finished."); +// return; +// } + +// _isFinishProcessed = true; +// Log.print( +// "🏁 Ride Finished via $source. Price: ${driverList.length > 3 ? driverList[3] : 'N/A'}"); + +// // 2. تحديث الحالة +// currentRideState.value = RideState.finished; + +// // 3. تنظيف الموارد +// disposeRideSocket(); // إغلاق السوكيت +// _stopDriverLocationPolling(); // إيقاف تتبع الموقع +// if (Get.isRegistered()) { +// Get.find().stopRecording(); +// } + +// // إغلاق أي ديالوج سابق +// if (Get.isDialogOpen == true) Get.back(); + +// // 4. إشعار "لا تنسى أغراضك" +// NotificationController().showNotification( +// 'Alert'.tr, +// "Please make sure not to leave any personal belongings in the car.".tr, +// 'tone1', +// ); +// IosLiveActivityService.endRideActivity(); +// PipService.disablePip(); // ✅ إيقاف PiP +// await RideLiveNotification.cancel(); +// // 5. استخراج البيانات والانتقال +// if (driverList.length >= 4) { +// String price = driverList[3].toString(); + +// // الانتقال لصفحة التقييم +// Get.offAll(() => RateDriverFromPassenger(), arguments: { +// 'driverId': driverList[0].toString(), +// 'rideId': driverList[1].toString(), +// 'price': price +// }); +// } +// } + +// void _startDriverLocationPollingWithTimer() { +// Log.print("📍 Starting Driver Location Polling (6s interval)"); + +// _locationPollingTimer?.cancel(); + +// // استدعاء فوري لأول مرة +// // getDriverCarsLocationToPassengerAfterApplied(); + +// _locationPollingTimer = Timer.periodic(Duration(seconds: 6), (timer) { +// // شرط التوقف: إذا انتهت الرحلة أو ألغيت +// if (currentRideState.value == RideState.finished || +// currentRideState.value == RideState.cancelled || +// currentRideState.value == RideState.noRide) { +// timer.cancel(); +// return; +// } + +// // جلب الموقع وتحديث الماركر +// getDriverCarsLocationToPassengerAfterApplied(); +// }); +// } + +// void _stopDriverLocationPolling() { +// Log.print("🛑 Stopping Location Polling"); +// _locationPollingTimer?.cancel(); +// _locationPollingTimer = null; +// } + +// // ============================================================================== +// // 5. التنظيف والإغلاق +// // ============================================================================== +// void disposeRideSocket() { +// if (socket != null) { +// socket!.disconnect(); +// socket!.dispose(); +// // socket = null; +// isSocketConnected = false; +// Log.print("🔌 Socket Disposed"); +// } +// } + +// /// ============================================================================== +// /// 6. معالجة تحديث موقع السائق من السوكيت +// /// ============================================================================== +// void handleDriverLocationUpdate(dynamic data) { +// if (!isSocketConnected || data == null) return; +// // 🔥 1. تسجيل وقت استلام البيانات (تغذية الـ Watchdog) +// _lastSocketLocationTime = DateTime.now(); +// _socketLocationUpdatesCount++; + +// // 🧠 إذا وصلتنا 3 تحديثات ناجحة من السوكيت، أوقف البولينج (إن كان يعمل) +// if (_socketLocationUpdatesCount >= 3 && _locationPollingTimer != null) { +// Log.print("✅ Socket delivering locations reliably. Stopping polling."); +// _stopDriverLocationPolling(); +// } +// try { +// double lat = double.tryParse(data['latitude']?.toString() ?? '0') ?? 0; +// double lng = double.tryParse(data['longitude']?.toString() ?? '0') ?? 0; +// double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0; + +// if (lat == 0 || lng == 0) return; + +// LatLng newPos = LatLng(lat, lng); + +// // تحديث القائمة (نفس المنطق القديم) +// if (driverCarsLocationToPassengerAfterApplied.isEmpty) { +// driverCarsLocationToPassengerAfterApplied.add(newPos); +// } else { +// driverCarsLocationToPassengerAfterApplied[0] = newPos; +// } + +// currentLocationOfDrivers = newPos; +// // ------------------------------------------------------------------ +// // 🔥 2. Smart Rerouting Logic ( deviation > 50m ) +// // ------------------------------------------------------------------ +// if (_currentDriverRoutePoints.isNotEmpty) { +// double deviation = +// _calculateDistanceToPolyline(newPos, _currentDriverRoutePoints); +// if (deviation > 50.0) { +// Log.print( +// "⚠️ Driver deviated by ${deviation.toStringAsFixed(1)}m. Smart Rerouting triggered."); +// // إعادة رسم المسار محلياً (لا يتم استدعاؤه إلا عند الانحراف الحقيقي) +// calculateDriverToPassengerRoute(newPos, passengerLocation); +// } +// } + +// // 🔥 تحديث الكاميرا: ضمان بقاء السيارة في منتصف الخريطة +// // ------------------------------------------------------------------ +// // ملاحظة: تأكد من أن mapController قد تم تهيئته (initialized) +// if (mapController != null) { +// // نستخدم animateCamera لحركة ناعمة بدلاً من moveCamera القاسية +// mapController!.animateCamera(CameraUpdate.newLatLng(newPos)); +// } +// // ------------------------------------------------------------------ + +// // تحديث الوقت المتبقي (إذا أرسله السيرفر) +// if (data['eta_seconds'] != null) { +// int etaSeconds = int.tryParse(data['eta_seconds'].toString()) ?? 0; +// if (etaSeconds > 0) { +// remainingTimeToPassengerFromDriverAfterApplied = etaSeconds; +// int minutes = (etaSeconds / 60).floor(); +// int seconds = etaSeconds % 60; +// stringRemainingTimeToPassenger = +// '$minutes:${seconds.toString().padLeft(2, '0')}'; +// } +// } + +// // تحديث الماركر +// _updateDriverMarker(newPos, heading); +// } catch (e) { +// Log.print('Error in handleDriverLocationUpdate: $e'); +// } +// } + +// // دالة مساعدة لتحديث ماركر السائق +// void _updateDriverMarker(LatLng position, double heading) { +// const String markerId = 'driver_location'; +// final mId = MarkerId(markerId); + +// final existingMarker = markers.cast().firstWhere( +// (m) => m?.markerId == mId, +// orElse: () => null, +// ); + +// if (existingMarker != null) { +// _smoothlyUpdateMarker(existingMarker, position, heading, carIcon); +// } else { +// markers = { +// ...markers, +// Marker( +// markerId: mId, +// position: position, +// icon: InlqBitmap.fromStyleImage(carIcon), +// rotation: heading, +// anchor: const Offset(0.5, 0.5), +// ), +// }; +// update(); +// } +// } + +// // === إضافة متغير للتحكم === +// bool _isUsingFallback = false; + +// void _startPollingFallback() { +// if (_isUsingFallback) return; + +// Log.print('🔄 Starting Polling Fallback Mode'); +// _isUsingFallback = true; + +// // استخدام _handleRideState الموجود (كما كان) +// _startMasterTimer(); +// } + +// void handleActiveRideOnStartup(dynamic data) { +// try { +// if (data == null || data['has_active_ride'] != true) { +// Log.print('[Startup] No active ride'); +// currentRideState.value = RideState.noRide; +// _startMasterTimer(); +// return; +// } + +// Log.print('[Startup] ✅ Active ride found!'); + +// var rideData = data['ride']; +// rideId = rideData['ride_id'].toString(); +// currentRideId = rideId; +// driverId = rideData['driver_id']?.toString() ?? ''; + +// String status = rideData['status']?.toString().toLowerCase() ?? ''; + +// // تحديد الحالة +// if (status == 'waiting' || status == 'searching') { +// currentRideState.value = RideState.searching; +// isSearchingWindow = true; +// } else if (status == 'apply' || status == 'applied') { +// currentRideState.value = RideState.driverApplied; +// statusRide = 'Apply'; + +// // الاشتراك في موقع السائق +// socket.emit('subscribe_driver_location', { +// 'ride_id': rideId, +// 'driver_id': driverId, +// }); + +// // استعادة بيانات السائق +// if (rideData['driver_info'] != null) { +// var dInfo = rideData['driver_info']; +// passengerName = dInfo['first_name']?.toString() ?? ''; +// driverPhone = dInfo['phone']?.toString() ?? ''; +// model = dInfo['model']?.toString() ?? ''; +// licensePlate = dInfo['license_plate']?.toString() ?? ''; +// } +// } else if (status == 'arrived') { +// currentRideState.value = RideState.driverArrived; +// statusRide = 'Arrived'; +// isDriverArrivePassenger = true; +// } else if (status == 'begin' || status == 'started') { +// currentRideState.value = RideState.inProgress; +// statusRide = 'Begin'; +// rideTimerBegin = true; + +// // استعادة Polyline +// if (rideData['polyline'] != null) { +// _restorePolyline(rideData['polyline']); +// } + +// rideIsBeginPassengerTimer(); +// } + +// update(); +// _startMasterTimer(); // للـ Safety Checks +// } catch (e) { +// Log.print('[Startup] Error: $e'); +// currentRideState.value = RideState.noRide; +// _startMasterTimer(); +// } +// } + +// Future _restorePolyline(String polylineString) async { +// try { +// List points = +// await compute(decodePolylineIsolate, polylineString); + +// polylineCoordinates.clear(); +// polylineCoordinates.addAll(points); + +// polyLines.clear(); +// polyLines = { +// ...polyLines, +// Polyline( +// polylineId: const PolylineId('route_direct'), +// points: polylineCoordinates, +// color: const Color(0xFF2196F3), +// width: 6, +// ) +// }; + +// update(); +// } catch (e) { +// Log.print('Error restoring polyline: $e'); +// } +// } + +// // متغير لمنع التكرار (Race Condition Guard) +// bool _isAcceptanceProcessed = false; + +// // ============================================================================== +// // الدالة الموحدة لمعالجة القبول (من السوكيت أو FCM) +// // ============================================================================== +// Future processRideAcceptance( +// {Map? driverData, required String source}) async { +// // 1. الحماية: إذا تم المعالجة مسبقاً، تجاهل +// // نستثني حالة واحدة: إذا كنا في وضع البحث (Searching) نقبل الأمر +// // إذا كنا في driverApplied، نتجاهل الأمر +// if (currentRideState.value != RideState.searching && +// _isAcceptanceProcessed) { +// Log.print("✋ Ignored Acceptance from $source. Already processed."); +// return; +// } + +// _rideAcceptedViaSource = source; +// _socketLocationUpdatesCount = 0; + +// _isAcceptanceProcessed = true; // قفل الباب +// Log.print("🚀 Winner: $source triggered acceptance! Processing..."); + +// // 2. إيقاف جميع التايمرات القديمة فوراً +// _masterTimer?.cancel(); +// // stopSearchingTimer(); // إذا كان لديك تايمر للبحث + +// // 3. تحديث الحالة في الواجهة +// currentRideState.value = RideState.driverApplied; +// statusRide = 'Apply'; +// isSearchingWindow = false; + +// // 4. معالجة البيانات (تعبئة المتغيرات) +// if (driverData != null && driverData.isNotEmpty) { +// Log.print("📥 Populating Data from $source payload..."); +// _fillDriverDataLocally(driverData); +// } else { +// Log.print("⚠️ No Data in Payload. Fallback to API."); +// await getUpdatedRideForDriverApply(rideId); +// } +// // أضف هذا بعد السطر الذي تستدعي فيه RideTrackingNative +// await IosLiveActivityService.startRideActivity( +// rideId: rideId, +// driverName: driverName ?? 'السائق', +// carDetails: '$make • $carColor', +// etaText: stringRemainingTimeToPassenger, +// progress: 0.0, +// ); +// // إشعارات (الأسعار، الأمان...) +// _showRideStartNotifications(); +// final etaText = stringRemainingTimeToPassenger; // مثال: "8 دقائق" +// final carInfo = '$make • $model • $licensePlate'; + +// await RideLiveNotification.showDriverOnWay( +// driverName: driverName, +// etaText: etaText, +// carInfo: carInfo, +// ); + +// update(); // تحديث الواجهة لإظهار بيانات السائق + +// // 5. 🔥 العمليات الجغرافية (المسار والوقت) 🔥 + +// // أ) جلب موقع السائق الأولي +// await getDriverCarsLocationToPassengerAfterApplied(); +// _startSocketWatchdog(); +// // ب) رسم المسار وحساب الوقت +// if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) { +// LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last; + +// // نستخدم الدالة الموحدة الجديدة للحساب والرسم +// await calculateDriverToPassengerRoute(driverPos, passengerLocation); + +// // ج) تشغيل التايمر المحلي (للعد التنازلي فقط) +// startTimerFromDriverToPassengerAfterApplied(); +// } +// final int timeToPassengerSeconds = +// timeToPassengerFromDriverAfterApplied; // مثلاً من السيرفر +// final double distanceDriverToPassengerMeters = +// double.tryParse(distanceByPassenger.toString()) ?? 0.0; + +// // [PiP] تفعيل PiP عند بدء الرحلة (سيدخل وضع النافذة العائمة عند خروج المستخدم) +// PipService.enablePip(); + +// // 6. 🔥 القرار الذكي: هل نبدأ البولينج أم نعتمد على السوكيت؟ 🔥 +// if (source == "Socket" && isSocketConnected) { +// // ✅ السوكيت هو من أخبرنا بالقبول — نثق به ولا نشغل البولينج +// Log.print( +// "🧠 Smart Mode: Socket accepted ride. Skipping polling, relying on WebSocket."); +// // الـ watchdog وحده يكفي كشبكة أمان +// } else { +// // ⚠️ القبول من FCM أو Polling — نشغل البولينج كالمعتاد +// Log.print("🔄 Fallback Mode: $source accepted ride. Starting polling."); +// _startDriverLocationPollingWithTimer(); +// } +// } + +// Timer? _watchdogTimer; + +// void _startSocketWatchdog() { +// _watchdogTimer?.cancel(); +// Log.print("👀 Starting Socket Watchdog (Hybrid Mode)..."); + +// _watchdogTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { +// if (currentRideState.value != RideState.driverApplied && +// currentRideState.value != RideState.driverArrived && +// currentRideState.value != RideState.inProgress) { +// timer.cancel(); +// return; +// } + +// final lastTime = _lastSocketLocationTime ?? +// DateTime.now().subtract(const Duration(minutes: 1)); +// final difference = DateTime.now().difference(lastTime).inSeconds; + +// if (difference < 15 && isSocketConnected) { +// // ✅ السوكيت صحي — تأكد أن البولينج متوقف إذا كنا في وضع السوكيت +// if (_locationPollingTimer != null && +// _rideAcceptedViaSource == "Socket") { +// Log.print("✅ Socket recovered. Stopping polling fallback."); +// _stopDriverLocationPolling(); +// } +// } else if (difference >= 15 && difference < 30) { +// // ⚠️ تأخر متوسط — جلب واحد فقط +// Log.print("⚠️ Socket silent for ${difference}s. Single API Poll..."); +// await getDriverCarsLocationToPassengerAfterApplied(); +// } else if (difference >= 30) { +// // 🔴 السوكيت ميت — تفعيل البولينج الكامل كـ fallback +// if (_locationPollingTimer == null) { +// Log.print( +// "🔴 Socket dead for ${difference}s. Activating polling fallback!"); +// _startDriverLocationPollingWithTimer(); +// } else { +// // البولينج يعمل بالفعل، فقط أكمل +// await getDriverCarsLocationToPassengerAfterApplied(); +// } +// } +// }); +// } + +// // قائمة بأنصاف الأقطار (بالأمتار) لكل مرحلة +// final List _searchRadii = [ +// 2400, +// 3000, +// 3100 +// ]; // 0 ثانية، 30 ثانية، 60 ثانية +// // المدة الزمنية لكل مرحلة بحث (بالثواني) +// final int _searchPhaseDurationSeconds = 30; +// // المهلة الإجمالية للبحث قبل عرض خيار زيادة السعر +// final int _totalSearchTimeoutSeconds = 90; // 90 ثانية +// // --- noRide throttling --- +// int _noRideSearchCount = 0; +// final int _noRideMaxTries = 3; // نفذ البحث 6 مرات فقط +// final int _noRideIntervalSec = 5; // بين كل محاولة وأخرى 5 ثواني +// DateTime? _noRideNextAllowed; // متى نسمح بالمحاولة التالية +// bool _noRideSearchCapped = false; // وصلنا للحد وتوقفنا +// // ============== new design to manage ride state ============== +// // === 1. حالة الرحلة والمؤقت الرئيسي (Single Source of Truth) === +// Rx currentRideState = RideState.noRide.obs; +// Timer? _masterTimer; +// final int _pollingIntervalSeconds = 13; // فاصل زمني موحد للاستعلام + +// void _startMasterTimer() { +// // نضمن أن مؤقت واحد فقط يعمل في أي وقت +// _masterTimer?.cancel(); +// _masterTimer = +// Timer.periodic(Duration(seconds: _pollingIntervalSeconds), (_) { +// _handleRideState(currentRideState.value); +// }); +// } + +// void stopAllTimers() { +// Log.print('🛑 FORCE STOP: Stopping ALL Timers and Streams 🛑'); + +// // 1. إيقاف الماكينة الرئيسية +// _masterTimer?.cancel(); +// _masterTimer = null; + +// // 2. إيقاف مؤقتات تتبع السائق +// timerToPassengerFromDriverAfterApplied?.cancel(); +// _timer?.cancel(); +// _uiCountdownTimer?.cancel(); + +// // 3. إيقاف مؤقتات الخريطة + +// _animationTimers.forEach((key, timer) => timer.cancel()); +// _animationTimers.clear(); + +// // 4. إغلاق الستريمز +// if (!_rideStatusStreamController.isClosed) +// _rideStatusStreamController.close(); +// if (!_beginRideStreamController.isClosed) +// _beginRideStreamController.close(); +// if (!timerController.isClosed) timerController.close(); + +// // 5. تصفير العدادات لمنع إعادة الدخول +// isTimerRunning = false; +// isBeginRideFromDriverRunning = false; +// _isFetchingDriverLocation = false; + +// update(); +// } + +// final int _maxNoRideSearch = 3; // عدد المرات القصوى +// final int _noRideDelaySeconds = 6; // الفاصل الزمني بين كل بحث +// // +// // +// // !!! يرجى استبدال الدالة القديمة بالكامل بهذه الدالة الجديدة !!! +// // +// // +// bool _isStateProcessing = false; + +// Future _handleRideState(RideState state) async { +// if (_isRatingScreenOpen) { +// Log.print('⛔ Rating Screen is Open. Skipping Logic.'); +// stopAllTimers(); // تأكيد إضافي للإيقاف +// return; +// } +// Log.print('Handling state: $state'); + +// // int effectivePollingInterval = _pollingIntervalSeconds; + +// // الحصول على الفاصل الزمني من الخريطة +// int effectivePollingInterval = +// _pollingIntervals[state] ?? _pollingIntervalSeconds; + +// switch (state) { +// case RideState.noRide: +// final now = DateTime.now(); +// if (_noRideSearchCount >= _noRideMaxTries) { +// if (!_noRideSearchCapped) { +// _noRideSearchCapped = true; +// Log.print('[noRide] search capped at $_noRideMaxTries attempts'); +// } +// break; +// } +// if (_noRideNextAllowed != null && now.isBefore(_noRideNextAllowed!)) { +// break; +// } +// _noRideSearchCount++; +// Log.print('_noRideSearchCount: $_noRideSearchCount'); +// _noRideNextAllowed = now.add(Duration(seconds: _noRideIntervalSec)); +// String currentCarType = box.read(BoxName.carType) ?? 'yet'; +// getCarsLocationByPassengerAndReloadMarker(); +// getNearestDriverByPassengerLocation(); +// break; + +// case RideState.cancelled: +// Log.print('[handleRideState] Ride cancelled. Stopping polling.'); +// stopAllTimers(); +// // effectivePollingInterval = 3600; +// break; + +// case RideState.preCheckReview: +// stopAllTimers(); +// _checkLastRideForReview(); +// break; + +// case RideState.searching: +// // Guard: Don't poll if ride is not registered yet +// if (rideId == 'yet' || rideId.isEmpty) break; + +// // 1. التحقق من حالة الطلب (هل قبله أحد؟) عبر البولينج كشبكة أمان +// try { +// String statusFromServer = await getRideStatus(rideId); +// if (statusFromServer == 'Apply' || statusFromServer == 'Applied') { +// await processRideAcceptance(source: "Polling"); +// break; +// } +// } catch (e) { +// Log.print('Error polling getRideStatus: $e'); +// } + +// final now = DateTime.now(); +// final int elapsedSeconds = now.difference(_searchStartTime!).inSeconds; + +// // انتهاء وقت البحث الكلي +// if (elapsedSeconds > _totalSearchTimeoutSeconds) { +// stopAllTimers(); +// currentRideState.value = RideState.noRide; +// isSearchingWindow = false; +// update(); +// _showIncreaseFeeDialog(); +// break; +// } + +// // 2. إدارة مراحل البحث (توسيع النطاق) +// // السيناريو الجديد: لا نقوم بالقصف العشوائي، نرسل بناء على المرحلة أو مرور وقت كافٍ لدخول سائقين جدد + +// int targetPhase = +// (elapsedSeconds / _searchPhaseDurationSeconds).floor(); +// if (targetPhase >= _searchRadii.length) { +// targetPhase = _searchRadii.length - 1; +// } + +// // هل تغيرت المرحلة (توسع النطاق)؟ أو هل مر 10 ثواني منذ آخر محاولة إرسال؟ +// // هذا يمنع إرسال الإشعار في كل دورة (كل 5 ثواني) ويقلل الازعاج +// bool isNewPhase = targetPhase > _currentSearchPhase; +// bool timeToScanForNewDrivers = +// (elapsedSeconds % 15 == 0); // كل 15 ثانية نفحص الدخول الجديد + +// if (isNewPhase || timeToScanForNewDrivers || elapsedSeconds < 5) { +// _currentSearchPhase = targetPhase; +// int currentRadius = _searchRadii[_currentSearchPhase]; + +// Log.print( +// '[Search Logic] Scanning for drivers. Phase: $_currentSearchPhase, Radius: $currentRadius'); +// // استدعاء دالة الإشعار الذكية +// // _findAndNotifyNearestDrivers(currentRadius); +// } + +// // تحديث نصوص الواجهة +// if (elapsedSeconds < 5) { +// driversStatusForSearchWindow = 'Your order is being prepared'.tr; +// } else if (elapsedSeconds < 15) { +// driversStatusForSearchWindow = 'Your order sent to drivers'.tr; +// } else { +// driversStatusForSearchWindow = +// 'The drivers are reviewing your request'.tr; +// } +// update(); +// break; + +// case RideState.driverApplied: +// // effectivePollingInterval = 10; +// if (!_isDriverAppliedLogicExecuted) { +// Log.print('[handleRideState] Execution driverApplied logic.'); +// rideAppliedFromDriver(true); +// _isDriverAppliedLogicExecuted = true; +// } + +// if (!isSocketConnected) { +// try { +// String statusFromServer = await getRideStatus(rideId); +// if (statusFromServer == 'Arrived') { +// currentRideState.value = RideState.driverArrived; +// break; +// } else if (statusFromServer == 'Begin' || +// statusFromServer == 'inProgress') { +// processRideBegin(); +// break; +// } +// } catch (e) { +// Log.print('Error polling for Arrived/Begin status: $e'); +// } +// } +// // 🧠 جلب الموقع فقط إذا السوكيت غير صحي +// if (!_isSocketHealthy()) { +// getDriverCarsLocationToPassengerAfterApplied(); +// } +// break; + +// case RideState.driverArrived: +// if (!_isDriverArrivedLogicExecuted) { +// _isDriverArrivedLogicExecuted = true; +// startTimerDriverWaitPassenger5Minute(); +// driverArrivePassengerDialoge(); +// } +// // effectivePollingInterval = 8; +// break; + +// case RideState.inProgress: +// // effectivePollingInterval = 6; + +// if (!isSocketConnected) { +// try { +// String statusFromServer = await getRideStatus(rideId); + +// // !!! هنا التغيير الجذري !!! +// if (statusFromServer == 'Finished' || +// statusFromServer == 'finished') { +// Log.print( +// '🏁 DETECTED FINISHED: Killing processes and forcing Review.'); + +// // 1. قتل العمليات فوراً +// stopAllTimers(); + +// // 2. تغيير الحالة الداخلية لمنع أي كود آخر من العمل +// currentRideState.value = RideState.preCheckReview; + +// // 3. تنظيف الواجهة +// tripFinishedFromDriver(); + +// // 4. استدعاء شاشة التقييم فوراً +// _checkLastRideForReview(); + +// return; // خروج نهائي من الدالة لمنع أي كود بالأسفل من التنفيذ +// } +// } catch (e) { +// Log.print('Error polling status: $e'); +// } +// } + +// // بقية كود التتبع العادي (لن يتم الوصول إليه إذا انتهت الرحلة) +// if (!_isRideBeginLogicExecuted) { +// _isRideBeginLogicExecuted = true; +// _executeBeginRideLogic(); +// } +// // 🧠 جلب الموقع فقط إذا السوكيت غير صحي +// if (!_isSocketHealthy()) { +// getDriverCarsLocationToPassengerAfterApplied(); +// } +// break; +// case RideState.finished: +// tripFinishedFromDriver(); +// stopAllTimers(); +// effectivePollingInterval = 3600; +// break; +// } +// // تحديث الماكينة الرئيسية إذا تغير الفاصل الزمني +// _startMasterTimerWithInterval(effectivePollingInterval); +// } + +// int _masterIntervalSeconds = -1; + +// void _startMasterTimerWithInterval(int seconds) { +// // نفس الانترفَل؟ لا تعمل شيء +// if (_masterTimer != null && _masterIntervalSeconds == seconds) return; + +// _masterIntervalSeconds = seconds; +// _masterTimer?.cancel(); + +// _masterTimer = Timer.periodic(Duration(seconds: seconds), (_) { +// _handleRideState(currentRideState.value); +// }); +// } + +// Future _checkInitialRideStatus() async { +// // 1. جلب الحالة من السيرفر (باستخدام getRideStatusFromStartApp) +// await getRideStatusFromStartApp(); +// String _status = rideStatusFromStartApp['data']['status']; +// // Log.print('rideStatusFromStartApp: ${rideStatusFromStartApp}'); +// // Log.print('_status: ${_status}'); + +// if (_status == 'waiting' || _status == 'Apply' || _status == 'Begin') { +// // رحلة جارية +// rideId = rideStatusFromStartApp['data']['rideId'].toString(); +// currentRideState.value = _status == 'waiting' +// ? RideState.searching +// : _status == 'Apply' +// ? RideState.driverApplied +// : _status == 'Begin' +// ? RideState.inProgress +// : _status == 'Cancel' +// ? RideState.cancelled +// : RideState.noRide; +// } else if (_status == 'Finished') { +// // رحلة منتهية/ملغاة +// if (rideStatusFromStartApp['data']['needsReview'] == 1) { +// currentRideState.value = RideState.preCheckReview; +// } else { +// currentRideState.value = RideState.noRide; +// } +// } else { +// currentRideState.value = RideState.noRide; +// } + +// // بدء المعالجة الفورية +// _handleRideState(currentRideState.value); +// } + +// Future _checkLastRideForReview() async { +// Log.print('⭐ FORCE OPEN RATING PAGE (Get.to mode)'); + +// // جلب البيانات +// await getRideStatusFromStartApp(); + +// if (rideStatusFromStartApp['data'] == null) { +// currentRideState.value = RideState.noRide; +// _startMasterTimer(); +// return; +// } + +// String needsReview = +// rideStatusFromStartApp['data']['needsReview'].toString(); + +// if (needsReview == '1') { +// _isRatingScreenOpen = true; +// // 1. تجهيز البيانات (Arguments) +// var args = { +// 'driverId': rideStatusFromStartApp['data']['driver_id'].toString(), +// 'rideId': rideStatusFromStartApp['data']['rideId'].toString(), +// 'driverName': rideStatusFromStartApp['data']['driverName'], +// 'price': rideStatusFromStartApp['data']['price'], +// }; + +// // 2. استخدام Get.to مع await (هذا هو الحل الجذري) +// // الكود سيتوقف هنا ولن يكمل التنفيذ حتى يتم إغلاق صفحة التقييم +// await Get.to( +// () => RatingDriverBottomSheet(), +// arguments: args, // تمرير البيانات بالطريقة التي تريدها +// preventDuplicates: true, // لمنع فتح الصفحة مرتين +// popGesture: false, // لمنع السحب للرجوع (في iOS) +// ); + +// // 3. هذا الكود لن يتنفذ إلا بعد أن يضغط المستخدم "تم" في التقييم ويغلق الصفحة +// Log.print('✅ Rating Page Closed. Resetting App.'); +// _isRatingScreenOpen = false; +// restCounter(); +// currentRideState.value = RideState.noRide; +// _startMasterTimer(); // إعادة تشغيل البحث الآن فقط +// } else { +// currentRideState.value = RideState.noRide; +// _startMasterTimer(); +// } +// } + +// void startSearchingForDriver() async { +// // ✅ منع الضغط المزدوج +// if (currentRideState.value == RideState.searching) { +// return; +// } +// // 1. تحديث الحالة الأولية +// isSearchingWindow = true; +// currentRideState.value = RideState.searching; +// driversStatusForSearchWindow = 'Searching for nearby drivers...'.tr; +// update(); + +// // 2. إرسال الطلب للسيرفر (add_ride.php) +// bool rideCreated = await postRideDetailsToServer(); + +// if (!rideCreated) { +// // فشل الإنشاء +// isSearchingWindow = false; +// currentRideState.value = RideState.noRide; +// mySnackbarWarning("Could not create ride. Please try again.".tr); +// update(); +// return; +// } + +// // 3. نجاح الإنشاء: إضافة لجدول الانتظار المحلي (اختياري حسب منطقك) +// _addRideToWaitingTable(); + +// // 4. 🔥 الاتصال بالسوكيت فوراً وانتظار الرد الحقيقي 🔥 +// // نغلق أي اتصال سابق ونبدأ اتصالاً جديداً مخصصاً لهذه الرحلة +// initConnectionWithSocket(); + +// // تشغيل الماكينة الرئيسية للمراقبة (كحماية إضافية) +// // _startMasterTimer(); +// } + +// /// دالة لإظهار النافذة المنبثقة لزيادة السعر +// void _showIncreaseFeeDialog() { +// Get.dialog( +// CupertinoAlertDialog( +// title: Text("No drivers accepted your request yet".tr), +// content: Text( +// "Increasing the fare might attract more drivers. Would you like to increase the price?" +// .tr), +// actions: [ +// CupertinoDialogAction( +// child: Text("Cancel Ride".tr, +// style: TextStyle(color: AppColor.redColor)), +// onPressed: () { +// Get.back(); +// changeCancelRidePageShow(); +// // cancelRide(); // دالة إلغاء الرحلة +// }, +// ), +// CupertinoDialogAction( +// child: Text("Increase Fare".tr, +// style: TextStyle(color: AppColor.greenColor)), +// onPressed: () { +// Get.back(); +// // هنا يمكنك عرض نافذة أخرى لإدخال السعر الجديد +// // وبعدها استدعاء الدالة التالية +// // كمثال، سنزيد السعر بنسبة 10% +// double newPrice = totalPassenger * 1.10; +// increasePriceAndRestartSearch(newPrice); +// }, +// ), +// ], +// ), +// barrierDismissible: false, +// ); +// } + +// /// دالة لتحديث السعر وإعادة بدء البحث +// Future increasePriceAndRestartSearch(double newPrice) async { +// totalPassenger = newPrice; +// update(); + +// // await CRUD().post(link: AppLink.updateRides, payload: { +// // "id": rideId, +// // "price": newPrice.toStringAsFixed(2), +// // }); +// CRUD().post(link: "${AppLink.server}/ride/rides/update.php", payload: { +// "id": rideId, +// "price": newPrice.toStringAsFixed(2), +// }); + +// // تصفير القائمة لأن السعر تغير، ويجب إبلاغ الجميع (حتى من وصله الإشعار سابقاً) +// Log.print( +// '[increasePrice] Price changed. Clearing notified list to resend.'); +// notifiedDrivers.clear(); + +// _searchStartTime = DateTime.now(); +// _currentSearchPhase = 0; + +// isSearchingWindow = true; +// update(); + +// _startMasterTimer(); +// } + +// /// (دالة جديدة موحدة) +// /// هذه هي الدالة الوحيدة المسؤولة عن بدء عملية قبول الرحلة. +// /// يتم استدعاؤها إما من إشعار Firebase أو من البحث الدوري (Polling). +// /// هي تحتوي على "حارس البوابة" لمنع تضارب السباق. +// /// (دالة موحدة وصارمة) +// /// تستدعى من FCM أو من Polling عند اكتشاف قبول السائق +// /// تمنع تضارب السباق وتضمن تنفيذ المنطق مرة واحدة فقط +// /// متغير لمنع التكرار (Race Condition Guard) + +// Future driverArrivePassengerDialoge() { +// return Get.defaultDialog( +// barrierDismissible: false, +// title: 'Hi ,I Arrive your site'.tr, +// titleStyle: AppStyle.title, +// middleText: 'Please go to Car Driver'.tr, +// middleTextStyle: AppStyle.title, +// confirm: MyElevatedButton( +// title: 'Ok I will go now.'.tr, +// onPressed: () { +// NotificationService.sendNotification( +// target: driverToken.toString(), +// title: 'Hi ,I will go now'.tr, +// body: 'I will go now'.tr, +// isTopic: false, // Important: this is a token +// tone: 'ding', +// driverList: [], +// category: 'Hi ,I will go now', +// ); + +// Get.back(); +// remainingTime = 0; +// update(); +// })); +// } + +// /// (دالة خاصة جديدة) +// /// تحتوي على كل المنطق الفعلي لبدء الرحلة. +// /// +// Timer? _waitPassengerTimer; +// static const int _waitPassengerTotalSeconds = 300; +// int _waitPassengerElapsedSeconds = 0; + +// /// **إيقاف عداد انتظار الراكب (Stop Wait Timer)** +// /// +// /// تقوم هذه الدالة بإلغاء التايمر النشط فوراً لتحرير الموارد ومنع تسريب الذاكرة. +// /// +// /// * [resetUI]: (اختياري) عند وضعه `true`، يتم تصفير العدادات وتحديث الواجهة لإخفاء التوقيت القديم. +// void _stopWaitPassengerTimer({bool resetUI = false}) { +// // 1. الإلغاء الآمن للتايمر (Safe Cancellation) +// _waitPassengerTimer?.cancel(); +// _waitPassengerTimer = null; + +// // 2. تصفير قيم الواجهة (Reset State) +// if (resetUI) { +// progressTimerDriverWaitPassenger5Minute = 0.0; +// remainingTimeDriverWaitPassenger5Minute = 0; +// stringRemainingTimeDriverWaitPassenger5Minute = '00:00'; + +// // ✅ تحديث الواجهة فوراً (GetX) +// update(); +// } +// } + +// void _executeBeginRideLogic() { +// Log.print('[executeBeginRideLogic] تنفيذ منطق بدء الرحلة...'); +// _stopWaitPassengerTimer(resetUI: true); // <-- إضافة + +// // 1. تصفير كل عدادات ما قبل الرحلة +// timeToPassengerFromDriverAfterApplied = 0; +// remainingTime = 0; +// remainingTimeToPassengerFromDriverAfterApplied = 0; +// remainingTimeDriverWaitPassenger5Minute = 0; + +// // 2. تحديث الحالة والواجهة +// rideTimerBegin = true; +// statusRide = 'Begin'; +// isDriverInPassengerWay = false; +// isDriverArrivePassenger = false; // لإخفاء واجهة "السائق وصل" + +// // 3. (من كود الإشعار الخاص بك) +// box.write(BoxName.passengerWalletTotal, '0'); +// update(); // تحديث الواجهة قبل بدء المؤقتات + +// // 4. بدء مؤقتات الرحلة الفعلية +// rideIsBeginPassengerTimer(); // مؤقت عداد مدة الرحلة +// // runWhenRideIsBegin(); // مؤقت تتبع موقع السائق أثناء الرحلة + +// // 5. إشعار الراكب (من كود الإشعار الخاص بك) +// NotificationController().showNotification( +// 'Trip is Begin'.tr, +// 'The trip has started! Feel free to contact emergency numbers, share your trip, or activate voice recording for the journey' +// .tr, +// 'start'); +// } + +// // متغير لمنع التكرار +// bool _isRideStartedProcessed = false; + +// /// **معالجة بدء الرحلة الموحدة (Unified Ride Start Handler)** +// /// +// /// تستدعى عند استلام حدث بدء الرحلة سواء من السوكيت أو FCM. +// /// تضمن انتقال التطبيق لحالة [RideState.inProgress] مرة واحدة فقط. +// Future processRideBegin({String source = "Unknown"}) async { +// // منطقك الحالي +// if (currentRideState.value == RideState.inProgress || +// _isRideStartedProcessed) { +// return; +// } + +// _isRideStartedProcessed = true; +// currentRideState.value = RideState.inProgress; +// statusRide = 'Begin'; + +// // إيقاف مؤقت الانتظار +// remainingTimeDriverWaitPassenger5Minute = 0; +// _stopWaitPassengerTimer(); + +// // 1) بيانات السائق والرحلة +// rideIsBeginPassengerTimer(); +// update(); +// } + +// late Duration durationToAdd; +// late DateTime newTime = DateTime.now(); +// int hours = 0; +// int minutes = 0; + +// // --- إضافة جديدة: للوصول إلى وحدة التحكم بالروابط --- +// final DeepLinkController _deepLinkController = +// Get.isRegistered() +// ? Get.find() +// : Get.put(DeepLinkController()); +// // ------------------------------------------------ + +// void onChangedPassengerCount(int newValue) { +// selectedPassengerCount = newValue; +// update(); +// } + +// void onChangedPassengersChoose() { +// isPassengerChosen = true; +// update(); +// } + +// void getCurrentLocationFormString() async { +// currentLocationToFormPlaces = true; +// currentLocationString = 'Waiting for your location'.tr; +// await getLocation(); +// currentLocationString = passengerLocation.toString(); +// newStartPointLocation = passengerLocation; +// update(); +// } + +// List coordinatesWithoutEmpty = []; +// void getMapPointsForAllMethods() async { +// clearPolyline(); +// isMarkersShown = false; +// isWayPointStopsSheetUtilGetMap = false; +// isWayPointSheet = false; +// durationToRide = 0; +// distanceOfDestination = 0; +// wayPointSheetHeight = 0; +// remainingTime = 25; +// haveSteps = true; + +// // Filter out empty value +// coordinatesWithoutEmpty = +// placesCoordinate.where((coord) => coord.isNotEmpty).toList(); +// latestPosition = LatLng( +// double.parse(coordinatesWithoutEmpty.last.split(',')[0]), +// double.parse(coordinatesWithoutEmpty.last.split(',')[1])); +// for (var i = 0; i < coordinatesWithoutEmpty.length; i++) { +// if ((i + 1) < coordinatesWithoutEmpty.length) { +// await getMapPoints( +// coordinatesWithoutEmpty[i].toString(), +// coordinatesWithoutEmpty[i + 1].toString(), +// i, +// ); +// if (i == 0) { +// startNameAddress = data[0]['start_address']; +// } +// if (i == coordinatesWithoutEmpty.length) { +// endNameAddress = data[0]['end_address']; +// } +// } +// } + +// // isWayPointStopsSheet = false; +// if (haveSteps) { +// String latestWaypoint = +// placesCoordinate.lastWhere((coord) => coord.isNotEmpty); +// latestPosition = LatLng( +// double.parse(latestWaypoint.split(',')[0]), +// double.parse(latestWaypoint.split(',')[1]), +// ); +// } +// updateCameraForDistanceAfterGetMap(); +// changeWayPointStopsSheet(); +// bottomSheet(); +// showBottomSheet1(); + +// update(); +// } + +// void convertHintTextPlaces(int index, var res) { +// if (placeListResponseAll[index].isEmpty) { +// placeListResponseAll[index] = res; +// hintTextwayPointStringAll[index] = 'Search for your Start point'.tr; +// update(); +// } else { +// hintTextwayPointStringAll[index] = res['name']; +// currentLocationStringAll[index] = res['name']; +// placesCoordinate[index] = +// '${res['geometry']['location']['lat']},${res['geometry']['location']['lng']}'; +// placeListResponseAll[index] = []; +// allTextEditingPlaces[index].clear(); +// // double lat = wayPoint0[index]['geometry']['location']['lat']; +// // double lng = wayPoint0[index]['geometry']['location']['lng']; +// // newPointLocation0 = LatLng(lat, lng); +// update(); +// Get.back(); +// } +// } + +// void convertHintTextDestinationNewPlaces(int index) { +// if (placesDestination.isEmpty) { +// hintTextDestinationPoint = 'Search for your destination'.tr; +// update(); +// } else { +// var res = placesDestination[index]; +// hintTextDestinationPoint = res['displayName']?['text'] ?? +// res['formattedAddress'] ?? +// 'Unknown Place'; +// double? lat = res['location']?['latitude']; +// double? lng = res['location']?['longitude']; + +// if (lat != null && lng != null) { +// newMyLocation = LatLng(lat, lng); +// // 🔥 الحل: تحريك الكاميرا فوراً للهدف حتى لا يتم مسحه عند إغلاق الكيبورد 🔥 +// mapController +// ?.animateCamera(CameraUpdate.newLatLngZoom(newMyLocation, 16)); +// } +// update(); +// } +// } + +// void convertHintTextDestinationNewPlacesFromRecent( +// List recentLocations, int index) { +// hintTextDestinationPoint = recentLocations[index]['name']; +// double lat = recentLocations[index]['latitude']; +// double lng = recentLocations[index]['longitude']; +// newMyLocation = LatLng(lat, lng); + +// // 🔥 تحريك الكاميرا فوراً 🔥 +// mapController?.animateCamera(CameraUpdate.newLatLngZoom(newMyLocation, 16)); +// update(); +// } + +// // final mainBottomMenuMap = GlobalKey(); +// void changeBottomSheetShown({bool? forceValue}) { +// if (forceValue != null) { +// isBottomSheetShown = forceValue; +// } else { +// isBottomSheetShown = !isBottomSheetShown; +// } + +// heightBottomSheetShown = isBottomSheetShown == true ? 250 : 0; +// update(); +// } + +// void changeCashConfirmPageShown() { +// isCashConfirmPageShown = !isCashConfirmPageShown; +// isCashSelectedBeforeConfirmRide = true; +// cashConfirmPageShown = isCashConfirmPageShown == true ? 250 : 0; +// // to get or sure picker point for origin //todo +// // isPickerShown = true; +// // clickPointPosition(); +// update(); +// } + +// void changePaymentMethodPageShown() { +// isPaymentMethodPageShown = !isPaymentMethodPageShown; +// paymentPageShown = isPaymentMethodPageShown == true ? Get.height * .6 : 0; +// update(); +// } + +// void changeMapType() { +// mapType = !mapType; +// // heightButtomSheetShown = isButtomSheetShown == true ? 240 : 0; +// update(); +// } + +// void changeMapTraffic() { +// mapTrafficON = !mapTrafficON; +// update(); +// } + +// void changeisAnotherOreder(bool val) { +// isAnotherOreder = val; +// update(); +// } + +// void changeIsWhatsAppOrder(bool val) { +// isWhatsAppOrder = val; +// update(); +// } + +// void sendSMS(String to) async { +// // Get the driver's phone number. +// String driverPhone = +// (dataCarsLocationByPassenger['message'][carsOrder]['phone'].toString()); + +// // Format the message. +// String message = +// 'Hi! This is ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n I am using ${box.read(AppInformation.appName)} to ride with $passengerName as the driver. $passengerName \nis driving a $model\n with license plate $licensePlate.\n I am currently located at $passengerLocation.\n If you need to reach me, please contact the driver directly at\n\n $driverPhone.'; + +// // Launch the URL to send the SMS. +// launchCommunication('sms', to, message); +// } + +// String formatSyrianPhone(String phone) { +// // Remove spaces and + +// phone = phone.replaceAll(' ', '').replaceAll('+', ''); + +// // If starts with 00963 → remove 00 → 963 +// if (phone.startsWith('00963')) { +// phone = phone.replaceFirst('00963', '963'); +// } + +// // If starts with 0963 (common mistake) → fix it +// if (phone.startsWith('0963')) { +// phone = phone.replaceFirst('0963', '963'); +// } + +// // If starts with 963 (already correct) +// if (phone.startsWith('963')) { +// return phone; // nothing to do +// } + +// // If starts with 09 → remove leading 0 → add 963 +// if (phone.startsWith('09')) { +// return '963' + phone.substring(1); // 9xxxxxxxxx +// } + +// // If starts with 9xxxxxxxxx (no country code) +// if (phone.startsWith('9') && phone.length == 9) { +// return '963' + phone; +// } + +// // Otherwise return raw phone +// return phone; +// } + +// void sendWhatsapp(String to) async { +// // Normalize phone number before sending +// String formattedPhone = formatSyrianPhone(to); + +// // Message body +// String message = +// '${'${'Hi! This is'.tr} ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n${' I am using'.tr}'} ${AppInformation.appName}${' to ride with'.tr} $passengerName${' as the driver.'.tr} $passengerName \n${'is driving a '.tr}$model\n${' with license plate '.tr}$licensePlate.\n${' I am currently located at '.tr} https://www.google.com/maps/place/${passengerLocation.latitude},${passengerLocation.longitude}.\n${' If you need to reach me, please contact the driver directly at'.tr}\n\n $driverPhone.'; + +// // Send WhatsApp message +// launchCommunication('whatsapp', formattedPhone, message); +// } + +// void changeCancelRidePageShow() { +// showCancelRideBottomSheet(); +// isCancelRidePageShown = !isCancelRidePageShown; +// // : cancelRide(); +// update(); +// } + +// void getDrawerMenu() { +// heightMenuBool = !heightMenuBool; +// widthMapTypeAndTraffic = heightMenuBool == true ? 0 : 50; +// heightMenu = heightMenuBool == true ? 80 : 0; +// widthMenu = heightMenuBool == true ? 110 : 0; +// update(); +// } + +// calcualateDistsanceInMetet(LatLng prev, current) async { +// double distance2 = Geolocator.distanceBetween( +// prev.latitude, +// prev.longitude, +// current.latitude, +// current.longitude, +// ); +// return distance2; +// } + +// StreamController _timerStreamController = StreamController(); +// Stream get timerStream => _timerStreamController.stream; +// bool isTimerFromDriverToPassengerAfterAppliedRunning = true; +// bool isTimerRunning = false; // Flag to track if the timer is running +// int beginRideInterval = 10; // Interval in seconds for getBeginRideFromDriver + +// void startTimerFromDriverToPassengerAfterApplied() { +// stopTimerFromDriverToPassengerAfterApplied(); +// if (isTimerRunning) return; +// isTimerRunning = true; +// isTimerFromDriverToPassengerAfterAppliedRunning = true; + +// int secondsElapsed = 0; + +// // استدعاء فوري لأول مرة +// // getDriverCarsLocationToPassengerAfterApplied(); + +// Timer.periodic(const Duration(seconds: 1), (timer) { +// // --- التغيير الجوهري هنا --- +// // شرط الإيقاف: نتوقف فقط إذا انتهت الرحلة أو ألغيت، أو تم إيقاف التايمر يدوياً +// // لم نعد نعتمد على تجاوز الوقت المقدر (timeToPassenger) كشرط للإيقاف +// bool isRideActive = (statusRide == 'Apply' || +// statusRide == 'Arrived' || +// statusRide == 'Begin' || +// currentRideState.value == RideState.driverApplied || +// currentRideState.value == RideState.driverArrived || +// currentRideState.value == RideState.inProgress); + +// if (!isRideActive || !isTimerFromDriverToPassengerAfterAppliedRunning) { +// timer.cancel(); +// isTimerRunning = false; +// if (!_timerStreamController.isClosed) { +// _timerStreamController.close(); +// } +// return; +// } + +// secondsElapsed++; +// if (!_timerStreamController.isClosed) { +// _timerStreamController.add(secondsElapsed); +// } + +// // تحديث الواجهة للوقت المتبقي (شكلياً فقط للراكب) +// // حتى لو أصبح الوقت سالباً (تأخر السائق)، سنظهره كـ 00:00 أو نتركه سالباً +// remainingTimeToPassengerFromDriverAfterApplied = +// timeToPassengerFromDriverAfterApplied - secondsElapsed; + +// if (remainingTimeToPassengerFromDriverAfterApplied < 0) { +// remainingTimeToPassengerFromDriverAfterApplied = 0; +// } + +// int minutes = +// (remainingTimeToPassengerFromDriverAfterApplied / 60).floor(); +// int seconds = remainingTimeToPassengerFromDriverAfterApplied % 60; +// stringRemainingTimeToPassenger = +// '$minutes:${seconds.toString().padLeft(2, '0')}'; +// // تحويل الوقت أو المسافة إلى نسبة من 0.0 إلى 1.0 +// double currentProgress = 1 - +// (remainingTimeToPassengerFromDriverAfterApplied / +// timeToPassengerFromDriverAfterApplied); + +// // 🔴 التعديل هنا: نحدث الآيفون كل 5 ثواني فقط للحفاظ على البطارية وتجنب حظر أبل +// if (secondsElapsed % 5 == 0) { +// double currentProgress = 1 - +// (remainingTimeToPassengerFromDriverAfterApplied / +// (timeToPassengerFromDriverAfterApplied == 0 +// ? 1 +// : timeToPassengerFromDriverAfterApplied)); + +// IosLiveActivityService.updateRideActivity( +// status: 'waiting', +// driverName: driverName ?? 'السائق', +// carDetails: +// '$make • $model • $carColor', // من الأفضل إظهار اللون أيضاً +// etaText: stringRemainingTimeToPassenger, +// progress: currentProgress.clamp(0.0, 1.0), +// ); +// } +// // جلب موقع السائق كل 4 ثواني (Polling) ما دامت الرحلة نشطة +// if (secondsElapsed % beginRideInterval == 0) { +// // 2. تحديث موقع الراكب للسائق +// uploadPassengerLocation(); +// } else { +// update(); +// } +// }); +// } + +// // Function to stop the timer +// void stopTimerFromDriverToPassengerAfterApplied() { +// isTimerFromDriverToPassengerAfterAppliedRunning = false; +// update(); +// } + +// void startTimerDriverWaitPassenger5Minute() { +// // لا تبدأ إلا إذا فعلاً وصلنا +// if (currentRideState.value != RideState.driverArrived) return; + +// // 1) أوقف أي عداد سابق (تتبع وصول السائق) +// stopTimerFromDriverToPassengerAfterApplied(); +// isTimerRunning = false; + +// // 2) أوقف عداد الانتظار إن كان شغال من قبل (منع تكرار) +// _stopWaitPassengerTimer(); + +// // 3) جهّز UI الانتظار +// isDriverArrivePassenger = true; +// isDriverInPassengerWay = false; +// timeToPassengerFromDriverAfterApplied = 0; + +// _waitPassengerElapsedSeconds = 0; +// remainingTimeDriverWaitPassenger5Minute = _waitPassengerTotalSeconds; +// progressTimerDriverWaitPassenger5Minute = 0; + +// int m = (remainingTimeDriverWaitPassenger5Minute / 60).floor(); +// int s = remainingTimeDriverWaitPassenger5Minute % 60; +// stringRemainingTimeDriverWaitPassenger5Minute = +// '$m:${s.toString().padLeft(2, '0')}'; + +// update(); + +// // 4) ابدأ Timer.periodic (يمكن إلغاؤه فوراً) +// _waitPassengerTimer = Timer.periodic(const Duration(seconds: 1), (t) { +// // أول ما تتحول إلى inProgress (أو أي حالة غير arrived) أوقف فوراً +// if (currentRideState.value != RideState.driverArrived) { +// _stopWaitPassengerTimer(resetUI: true); +// // إخفاء واجهة "السائق وصل" إذا بدأت الرحلة +// if (currentRideState.value == RideState.inProgress) { +// isDriverArrivePassenger = false; +// } +// update(); +// return; +// } + +// _waitPassengerElapsedSeconds++; +// int remaining = _waitPassengerTotalSeconds - _waitPassengerElapsedSeconds; +// if (remaining < 0) remaining = 0; + +// remainingTimeDriverWaitPassenger5Minute = remaining; +// progressTimerDriverWaitPassenger5Minute = +// _waitPassengerElapsedSeconds / _waitPassengerTotalSeconds; + +// int minutes = (remaining / 60).floor(); +// int seconds = remaining % 60; +// stringRemainingTimeDriverWaitPassenger5Minute = +// '$minutes:${seconds.toString().padLeft(2, '0')}'; + +// update(); + +// if (remaining == 0) { +// _stopWaitPassengerTimer(); +// // هنا إذا بدك: طبّق غرامة انتظار / اعرض رسالة / إلخ +// } +// }); +// } + +// // Create a StreamController to manage the timer values +// final timerController = StreamController(); + +// // Start the timer when the ride begins +// void beginRideTimer() { +// // Set up the timer to run every second +// Timer.periodic(const Duration(seconds: 1), (timer) { +// // Update the timer value and notify listeners +// timerController.add(timer.tick); +// update(); +// }); +// } + +// // Stop the timer when the ride ends +// void stopRideTimer() { +// timerController.close(); +// update(); +// } + +// Timer? _rideProgressTimer; +// bool _hasShownSpeedWarning = false; // متغير لحالة التنبيه + +// /// **بدء مؤقت الرحلة للراكب (Passenger Ride Timer)** +// /// +// /// تقوم هذه الدالة بإدارة العداد الزمني للرحلة بمجرد بدئها (حالة [RideState.inProgress]). +// /// +// /// **المهام الرئيسية:** +// /// 1. **دقة التوقيت:** تعتمد على فرق الوقت الحقيقي (`DateTime.difference`) لضمان دقة العداد حتى لو خرج المستخدم من التطبيق وعاد. +// /// 2. **مراقبة السرعة:** تفحص سرعة المركبة كل ثانية، وتطلق تحذيراً [_triggerSpeedWarning] إذا تجاوزت 100 كم/س. +// /// 3. **تحديث الواجهة:** تقوم بتحديث شريط التقدم والوقت المتبقي لحظياً. +// /// 4. **الإيقاف التلقائي:** تتوقف تلقائياً عند انتهاء الوقت أو تغير حالة الرحلة. +// void rideIsBeginPassengerTimer() { +// // 1. تنظيف أي تايمر سابق +// _rideProgressTimer?.cancel(); +// _hasShownSpeedWarning = false; // تصفير حالة التنبيه + +// // 2. تحديد وقت الوصول المتوقع بدقة +// DateTime now = DateTime.now(); +// DateTime expectedArrivalTime = now.add(Duration(seconds: durationToRide)); + +// // تنسيق وقت الوصول للعرض +// var arrivalTime = DateFormat('hh:mm a').format(expectedArrivalTime); +// box.write(BoxName.arrivalTime, arrivalTime); + +// Log.print("⏳ Ride Timer Started. Duration: $durationToRide sec"); + +// // 3. بدء التايمر الدوري +// _rideProgressTimer = +// Timer.periodic(const Duration(seconds: 1), (timer) async { +// // أ) شرط الإيقاف الحاسم: إذا انتهت الرحلة أو ألغيت +// if (currentRideState.value != RideState.inProgress) { +// timer.cancel(); +// return; +// } + +// // ب) حساب الوقت المتبقي بناءً على الساعة الحالية (أدق من العد) +// DateTime currentNow = DateTime.now(); +// int remainingSeconds = +// expectedArrivalTime.difference(currentNow).inSeconds; + +// if (remainingSeconds < 0) remainingSeconds = 0; + +// // تحديث المتغيرات +// remainingTimeTimerRideBegin = remainingSeconds; + +// // حساب النسبة المئوية (حماية من القسمة على صفر) +// progressTimerRideBegin = +// durationToRide > 0 ? 1 - (remainingSeconds / durationToRide) : 1.0; + +// // ج) تنسيق الوقت للعرض +// int minutes = (remainingSeconds / 60).floor(); +// int seconds = remainingSeconds % 60; +// stringRemainingTimeRideBegin = +// '$minutes:${seconds.toString().padLeft(2, '0')}'; + +// // نحول progressTimerRideBegin (0..1) إلى نسبة (0..100) +// final percent = (progressTimerRideBegin * 100).clamp(0, 100).toInt(); + +// // ============================================================== +// // 🔔 د) تحديث الإشعارات (هنا تم حل مشكلة الإزعاج) +// // ============================================================== + +// // 1. تحديث الآيفون (Live Activity): يمكن تحديثه كل 5 ثواني لأنه "تحديث صامت" للشاشة فقط ولا يصدر صوتاً +// if (remainingSeconds % 5 == 0 || remainingSeconds == 0) { +// IosLiveActivityService.updateRideActivity( +// status: 'ongoing', // ['waiting', 'ongoing'] +// driverName: driverName ?? '', +// carDetails: '$make • $model • $carColor', +// etaText: stringRemainingTimeRideBegin, +// progress: progressTimerRideBegin.clamp(0.0, 1.0), +// ); +// } + +// // 2. تحديث إشعار الهاتف العادي (RideLiveNotification): +// // نحدثه كل دقيقة (60 ثانية) بدلاً من 5 ثواني حتى لا يزعج الراكب بالرنين المستمر! +// if (remainingSeconds % 60 == 0 || remainingSeconds == 0) { +// await RideLiveNotification.showTripInProgress( +// percentage: percent, +// etaText: stringRemainingTimeRideBegin, +// ); +// } +// // ============================================================== + +// // هـ) منطق الإشعارات لمنتصف الرحلة (يصدر تنبيه مرة واحدة فقط) +// if (progressTimerRideBegin >= 0.25 && +// progressTimerRideBegin < 0.26 && +// !_hasShownSpeedWarning) { +// // يمكن إضافة منطق إشعار منتصف الرحلة هنا +// } + +// // و) مراقبة السرعة (Speed Check) +// if (speed > 100 && !_hasShownSpeedWarning) { +// _hasShownSpeedWarning = true; // ✅ قفل التنبيه حتى لا يتكرر +// _triggerSpeedWarning(); +// } + +// // إعادة تفعيل التنبيه إذا انخفضت السرعة (إعادة ضبط الأمان) +// if (speed < 80 && _hasShownSpeedWarning) { +// _hasShownSpeedWarning = false; +// } + +// // ز) إنهاء التايمر إذا انتهى الوقت +// if (remainingSeconds <= 0) { +// timer.cancel(); +// } + +// update(); +// }); +// } + +// /// **عرض تحذير السرعة الزائدة (Speed Warning Trigger)** +// /// +// /// تظهر نافذة منبثقة (Dialog) وإشعاراً محلياً لتحذير الراكب عند اكتشاف سرعة عالية (> 100 كم/س). +// /// +// /// **الخيارات المتاحة للمستخدم:** +// /// * **مشاركة التفاصيل:** لإرسال رسالة استغاثة عبر واتساب. +// /// * **أنا بخير:** لإغلاق التنبيه والاستمرار في الرحلة. +// void _triggerSpeedWarning() { +// NotificationController().showNotification("Warning: Speeding detected!".tr, +// 'You can call or record audio of this trip'.tr, 'tone1'); + +// Get.defaultDialog( +// barrierDismissible: false, +// title: "Warning: Speeding detected!".tr, +// titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), +// content: Column( +// children: [ +// Icon(Icons.speed, size: 50, color: AppColor.redColor), +// const SizedBox(height: 10), +// Text( +// "We noticed the speed is exceeding 100 km/h. Please slow down for your safety..." +// .tr, +// textAlign: TextAlign.center, +// style: AppStyle.title, +// ), +// ], +// ), +// confirm: MyElevatedButton( +// title: "Share Trip Details".tr, +// kolor: AppColor.redColor, +// onPressed: () { +// Get.back(); +// _shareTripDetailsSOS(); +// }, +// ), +// cancel: MyElevatedButton( +// title: "I'm Safe".tr, +// kolor: AppColor.greenColor, +// onPressed: () { +// Get.back(); +// }, +// ), +// ); +// } + +// /// **تفعيل وضع الطوارئ للمركبة (sosPassenger)** +// /// +// /// تقوم بإظهار حوار تأكيدي للمستخدم لسؤاله عما إذا كان يرغب في إرسال +// /// إشارة استغاثة عبر واتساب. +// void sosPassenger() { +// Get.defaultDialog( +// barrierDismissible: false, +// title: "Emergency SOS".tr, +// titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), +// content: Column( +// children: [ +// Icon(Icons.warning_amber_rounded, size: 50, color: AppColor.redColor), +// const SizedBox(height: 10), +// Text( +// "Do you want to send an emergency message to your SOS contact?".tr, +// textAlign: TextAlign.center, +// style: AppStyle.title, +// ), +// ], +// ), +// confirm: MyElevatedButton( +// title: "Send SOS".tr, +// kolor: AppColor.redColor, +// onPressed: () { +// Get.back(); +// _shareTripDetailsSOS(); +// }, +// ), +// cancel: MyElevatedButton( +// title: "I'm Safe".tr, +// kolor: AppColor.greenColor, +// onPressed: () { +// Get.back(); +// }, +// ), +// ); +// } + +// /// **مشاركة تفاصيل الرحلة للطوارئ (SOS Share)** +// /// +// /// تقوم بتجهيز رسالة نصية مفصلة تحتوي على بيانات الرحلة الحالية وإرسالها +// /// عبر تطبيق WhatsApp لرقم الطوارئ المحفوظ. +// /// +// /// **البيانات المرسلة:** +// /// * موقع الانطلاق والوصول. +// /// * اسم السائق، رقم الهاتف، ونوع السيارة. +// /// * رابط مباشر للموقع الحالي على خرائط جوجل. +// void _shareTripDetailsSOS() { +// String message = "**Emergency SOS from Passenger:**\n"; +// String origin = startNameAddress; +// String destination = endNameAddress; + +// message += "* ${'Origin'.tr}: $origin\n"; +// message += "* ${'Destination'.tr}: $destination\n"; +// message += "* ${'Driver Name'.tr}: $driverName\n"; +// message += "* ${'Car'.tr}: $make - $model - $licensePlate\n"; +// message += "* ${'Phone'.tr}: $driverPhone\n\n"; + +// // رابط جوجل مابس صحيح +// message += +// "${'Location'.tr}: https://www.google.com/maps/search/?api=1&query=${passengerLocation.latitude},${passengerLocation.longitude}\n"; +// message += "Please help! Contact me as soon as possible.".tr; + +// launchCommunication( +// 'whatsapp', box.read(BoxName.sosPhonePassenger), message); +// } + +// int progressTimerRideBeginVip = 0; +// int elapsedTimeInSeconds = 0; // Timer starts from 0 +// String stringElapsedTimeRideBegin = '0:00'; +// String stringElapsedTimeRideBeginVip = '0:00'; +// bool rideInProgress = true; // To control when to stop the timer + +// void rideIsBeginPassengerTimerVIP() async { +// rideInProgress = true; // Start the ride timer +// bool sendSOS = false; +// while (rideInProgress) { +// await Future.delayed(const Duration(seconds: 1)); + +// // Increment elapsed time +// elapsedTimeInSeconds++; + +// // Update the time display +// int minutes = (elapsedTimeInSeconds / 60).floor(); +// int seconds = elapsedTimeInSeconds % 60; +// stringElapsedTimeRideBeginVip = +// '$minutes:${seconds.toString().padLeft(2, '0')}'; + +// // Check for speed and SOS conditions +// if (speed > 100 && !sendSOS) { +// Get.defaultDialog( +// barrierDismissible: false, +// title: "Warning: Speeding detected!".tr, +// titleStyle: AppStyle.title, +// content: Text( +// "We noticed the speed is exceeding 100 km/h. Please slow down for your safety. If you feel unsafe, you can share your trip details with a contact or call the police using the red SOS button." +// .tr, +// style: AppStyle.title, +// ), +// confirm: MyElevatedButton( +// title: "Share Trip Details".tr, +// onPressed: () { +// Get.back(); +// // Implement sharing trip details logic here +// String message = "**Emergency SOS from Passenger:**\n"; + +// // Get trip details from GetX or relevant provider +// String origin = passengerLocation.toString(); +// String destination = myDestination.toString(); +// String driverName = passengerName; +// String driverCarPlate = licensePlate; + +// // Add trip details to the message +// message += "* ${'Origin'.tr}: $origin\n"; +// message += "* ${'Destination'.tr}: $destination\n"; +// message += "* ${'Driver Name'.tr}: $driverName\n"; +// message += "* ${'Driver Car Plate'.tr}: $driverCarPlate\n\n"; +// message += "* ${'Driver Phone'.tr}: $driverPhone\n\n"; + +// // Add current location +// message += +// "${'Current Location'.tr}:https://www.google.com/maps/place/${passengerLocation.latitude},${passengerLocation.longitude} \n"; + +// // Append a call to action +// message += "Please help! Contact me as soon as possible.".tr; + +// // Launch WhatsApp communication +// launchCommunication( +// 'whatsapp', box.read(BoxName.sosPhonePassenger), message); +// sendSOS = true; +// }, +// kolor: AppColor.redColor, +// ), +// cancel: MyElevatedButton( +// title: "Cancel".tr, +// onPressed: () { +// Get.back(); +// }, +// kolor: AppColor.greenColor, +// ), +// ); +// } + +// // Update the UI +// update(); +// } +// } + +// Future tripFinishedFromDriver() async { +// Log.print('🧹 Cleaning UI for Finish'); + +// // إغلاق أي ديالوج مفتوح +// if (Get.isDialogOpen == true) Get.back(); +// if (Get.isBottomSheetOpen == true) Get.back(); + +// statusRide = 'Finished'; +// currentRideState.value = RideState.finished; // تثبيت الحالة + +// // إيقاف البحث والعدادات +// isSearchingWindow = false; +// rideTimerBegin = false; +// shouldFetch = false; + +// // إيقاف التايمرات +// stopAllTimers(); +// resetAllMapStates(); +// clearPolyline(); +// clearMarkersExceptStartEnd(); +// markers.clear(); + +// update(); +// } + +// StreamController _beginRideStreamController = +// StreamController.broadcast(); +// Stream get beginRideStream => _beginRideStreamController.stream; + +// bool isBeginRideFromDriverRunning = false; + +// // Call this method to listen to the stream +// void listenToBeginRideStream() { +// beginRideStream.listen((status) { +// Log.print("Ride status: $status"); +// // Perform additional actions based on the status +// }, onError: (error) { +// Log.print("Error in Begin Ride Stream: $error"); +// }); +// } + +// begiVIPTripFromPassenger() async { +// timeToPassengerFromDriverAfterApplied = 0; +// remainingTime = 0; +// isBottomSheetShown = false; +// remainingTimeToPassengerFromDriverAfterApplied = 0; +// remainingTimeDriverWaitPassenger5Minute = 0; +// rideTimerBegin = true; +// statusRideVip = 'Begin'; +// isDriverInPassengerWay = false; +// isDriverArrivePassenger = false; +// update(); +// // isCancelRidePageShown = true; +// rideIsBeginPassengerTimerVIP(); +// runWhenRideIsBegin(); +// } + +// Map rideStatusFromStartApp = {}; +// bool isStartAppHasRide = false; +// getRideStatusFromStartApp() async { +// try { +// var res = await CRUD().get( +// link: AppLink.getRideStatusFromStartApp, +// payload: {'passenger_id': box.read(BoxName.passengerID)}); +// // Log.print(res); +// Log.print('rideStatusFromStartApp: $res'); +// // Log.print('1070'); +// if (res == 'failure') { +// rideStatusFromStartApp = { +// 'data': {'status': 'NoRide', 'needsReview': false} +// }; +// isStartAppHasRide = false; +// Log.print( +// "No rides found for the given passenger ID within the last hour."); +// } else { +// var decoded = jsonDecode(res); +// if (decoded['status'] == 'failure') { +// rideStatusFromStartApp = { +// 'data': {'status': 'NoRide', 'needsReview': false} +// }; +// isStartAppHasRide = false; +// } else { +// rideStatusFromStartApp = decoded; +// } +// } +// if (rideStatusFromStartApp['data']['status'] == 'Begin' || +// rideStatusFromStartApp['data']['status'] == 'Apply' || +// rideStatusFromStartApp['data']['status'] == 'Applied') { +// statusRide = rideStatusFromStartApp['data']['status']; +// isStartAppHasRide = true; +// RideState.inProgress; +// driverId = rideStatusFromStartApp['data']['driver_id']; +// passengerName = rideStatusFromStartApp['data']['driverName']; +// driverRate = rideStatusFromStartApp['data']['rateDriver'].toString(); +// statusRideFromStart = true; + +// update(); + +// Map tripData = +// box.read(BoxName.tripData) as Map; +// final String pointsString = tripData['polyline']; +// List decodedPoints = +// await compute(decodePolylineIsolate, pointsString); + +// // decodePolyline(response["routes"][0]["overview_polyline"]["points"]); +// for (int i = 0; i < decodedPoints.length; i++) { +// polylineCoordinates.add(decodedPoints[i]); +// } +// var polyline = Polyline( +// polylineId: const PolylineId('main_route'), +// points: polylineCoordinates, +// width: 6, +// color: const Color(0xFF2196F3), +// ); + +// polyLines = {...polyLines, polyline}; +// timeToPassengerFromDriverAfterApplied = 0; +// remainingTime = 0; +// remainingTimeToPassengerFromDriverAfterApplied = 0; +// remainingTimeDriverWaitPassenger5Minute = 0; +// rideTimerBegin = true; +// isDriverInPassengerWay = false; +// isDriverArrivePassenger = false; +// // update(); +// // isCancelRidePageShown = true; +// durationToAdd = tripData['distance_m']; +// rideIsBeginPassengerTimer(); +// runWhenRideIsBegin(); +// update(); +// } +// } catch (e) { +// // Handle the error or perform any necessary actions +// } +// } + +// void driverArrivePassenger() { +// timeToPassengerFromDriverAfterApplied = 0; +// remainingTime = 0; +// // isCancelRidePageShown = true; +// update(); +// rideIsBeginPassengerTimer(); +// // runWhenRideIsBegin(); +// } + +// void cancelTimerToPassengerFromDriverAfterApplied() { +// timerToPassengerFromDriverAfterApplied?.cancel(); +// } + +// void clearPlacesDestination() { +// placesDestination = []; +// hintTextDestinationPoint = 'Search for your destination'.tr; +// update(); +// } + +// void clearPlacesStart() { +// placesStart = []; +// hintTextStartPoint = 'Search for your Start point'.tr; +// update(); +// } + +// void clearPlaces(int index) { +// placeListResponseAll[index] = []; +// hintTextwayPointStringAll[index] = 'Search for waypoint'.tr; +// update(); +// } + +// int selectedReason = -1; +// String? cancelNote; +// void selectReason0(int index, String note) { +// selectedReason = index; +// cancelNote = note; +// update(); +// } + +// void getDialog(String title, String? midTitle, VoidCallback onPressed) { +// final textToSpeechController = Get.find(); +// Get.defaultDialog( +// title: title, +// titleStyle: AppStyle.title, +// middleTextStyle: AppStyle.title, +// content: Column( +// children: [ +// IconButton( +// onPressed: () async { +// await textToSpeechController.speakText(title ?? midTitle!); +// }, +// icon: const Icon(Icons.headphones)), +// Text( +// midTitle!, +// style: AppStyle.title, +// ) +// ], +// ), +// confirm: MyElevatedButton( +// title: 'Ok'.tr, +// onPressed: onPressed, +// kolor: AppColor.greenColor, +// ), +// cancel: MyElevatedButton( +// title: 'Cancel', +// kolor: AppColor.redColor, +// onPressed: () { +// Get.back(); +// })); +// } + +// Future?> extractCoordinatesFromLinkAsync( +// String link) async { +// try { +// // 1. معالجة روابط الخرائط المباشرة (geo: و google.navigation:) +// if (link.startsWith('geo:') || link.startsWith('google.navigation:')) { +// RegExp regex = RegExp(r'(-?\d+\.\d+)[,/~=](-?\d+\.\d+)'); +// var match = regex.firstMatch(link); +// if (match != null) { +// double lat = double.parse(match.group(1)!); +// double lng = double.parse(match.group(2)!); +// if (lat > 40 && lat > lng) { +// double temp = lat; +// lat = lng; +// lng = temp; +// } +// return {'latitude': lat, 'longitude': lng}; +// } +// } + +// // 2. معالجة الروابط العادية (http/https) +// int urlStartIndex = link.indexOf(RegExp(r'https?://')); +// if (urlStartIndex == -1) return null; +// String cleanLink = link.substring(urlStartIndex).trim(); + +// Uri uri = Uri.parse(cleanLink); +// String finalUrl = cleanLink; + +// // فك التوجيه للروابط المختصرة +// if (cleanLink.contains('goo.gl') || +// cleanLink.contains('maps.google.com')) { +// try { +// var response = +// await http.get(uri).timeout(const Duration(seconds: 5)); +// finalUrl = response.request?.url.toString() ?? cleanLink; +// } catch (e) { +// Log.print('Redirect logic failed, using original: $e'); +// } +// } + +// // الأنماط المشتركة لخرائط جوجل (تكون دائماً Lat ثم Lng) +// RegExp regex = RegExp(r'(-?\d+\.\d+)[,/~](-?\d+\.\d+)'); +// var match = regex.firstMatch(finalUrl); + +// if (match != null) { +// double lat = double.parse(match.group(1)!); +// double lng = double.parse(match.group(2)!); + +// // 🔥 منطق التصحيح الذاتي (Smart Swap) للمنطقة (سوريا/الأردن/مصر) +// // إذا كان الرقم الأول أكبر من الرقم الثاني بشكل واضح، فهذا يعني أن الرابط مقلوب أو أننا نحتاج للتأكد +// // في منطقتنا Latitude حوالي 30-35 و Longitude حوالي 36-44 +// if (lat > 40 && lat > lng) { +// Log.print("⚠️ Detected Swapped Coordinates in Link. Correcting..."); +// double temp = lat; +// lat = lng; +// lng = temp; +// } + +// return { +// 'latitude': lat, +// 'longitude': lng, +// }; +// } +// } catch (e) { +// Log.print('Error parsing location link: $e'); +// } +// return null; +// } + +// double latitudeWhatsApp = 0; +// double longitudeWhatsApp = 0; +// void handleWhatsAppLink(String link) async { +// Map? coordinates = +// await extractCoordinatesFromLinkAsync(link); + +// if (coordinates != null) { +// latitudeWhatsApp = coordinates['latitude']!; +// longitudeWhatsApp = coordinates['longitude']!; + +// Log.print( +// 'Extracted coordinates: Lat: $latitudeWhatsApp, Long: $longitudeWhatsApp'); +// // Use these coordinates in your app as needed +// } else { +// Log.print('Failed to extract coordinates from the link'); +// } +// } + +// void goToWhatappLocation() async { +// if (sosFormKey.currentState!.validate()) { +// // 1. استخراج الإحداثيات أولاً بشكل محلي لضمان عدم حدوث سباق بيانات (Race Condition) +// Map? coordinates = +// await extractCoordinatesFromLinkAsync(whatsAppLocationText.text); + +// if (coordinates != null) { +// latitudeWhatsApp = coordinates['latitude']!; +// longitudeWhatsApp = coordinates['longitude']!; + +// Log.print( +// '📍 Final Coordinates for OSM: Lat: $latitudeWhatsApp, Lng: $longitudeWhatsApp'); + +// changeIsWhatsAppOrder(true); +// Get.back(); + +// // إعداد الوجهة +// myDestination = LatLng(latitudeWhatsApp, longitudeWhatsApp); + +// // تحريك الكاميرا لموقع الراكب (البداية) وليس الوجهة فوراً لضمان تحميل الخريطة +// if (passengerLocation != null) { +// await mapController?.animateCamera(CameraUpdate.newLatLng( +// LatLng(passengerLocation.latitude, passengerLocation.longitude))); +// } + +// changeMainBottomMenuMap(); +// passengerStartLocationFromMap = true; +// isPickerShown = true; +// update(); +// } else { +// mySnackbarWarning('لم نتمكن من استخراج الموقع من الرابط'); +// } +// } +// } + +// int currentTimeSearchingCaptainWindow = 0; +// late String driverPhone = ''; +// late String driverRate = ''; +// late String passengerName = ''; +// late String carColor = ''; +// late String colorHex = ''; +// late String carYear = ''; +// late String model = ''; +// late String make = ''; +// late String licensePlate = ''; + +// String driverOrderStatus = 'yet'; +// bool isDriversTokensSend = false; + +// Set notifiedDrivers = {}; + +// /// [إضافة جديدة] +// /// دالة مخصصة لإضافة الرحلة إلى جدول الانتظار (waiting_ride) + +// Future _addRideToWaitingTable() async { +// try { +// await CRUD().post(link: AppLink.addWaitingRide, payload: { +// 'id': rideId.toString(), +// "start_location": +// '${startLocation.latitude},${startLocation.longitude}', +// "end_location": '${endLocation.latitude},${endLocation.longitude}', +// "date": DateTime.now().toString(), +// "time": DateTime.now().toString(), +// "price": totalPassenger.toStringAsFixed(2), +// 'passenger_id': box.read(BoxName.passengerID).toString(), +// 'status': 'waiting', // الحالة الرئيسية لجدول الانتظار +// 'carType': box.read(BoxName.carType), +// 'passengerRate': passengerRate.toStringAsFixed(2), +// 'price_for_passenger': totalME.toStringAsFixed(2), +// 'distance': distance.toStringAsFixed(1), +// 'duration': duration.toStringAsFixed(1), +// 'payment_method': +// Get.find().isWalletChecked ? 'wallet' : 'cash', +// "passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(), +// }); +// Log.print('[WaitingTable] Ride $rideId added to waiting_ride table.'); +// } catch (e) { +// Log.print('Error adding ride to waiting_ride table: $e'); +// } +// } + +// String driversStatusForSearchWindow = ''; + +// bool isDriversDataValid() { +// return dataCarsLocationByPassenger != 'failure' && +// dataCarsLocationByPassenger != null && +// dataCarsLocationByPassenger.containsKey('message') && +// dataCarsLocationByPassenger['message'] != null; +// } + +// void showNoDriversDialog() { +// Get.dialog( +// BackdropFilter( +// filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), +// child: CupertinoAlertDialog( +// title: Text("No Car or Driver Found in your area.".tr, +// style: AppStyle.title +// .copyWith(fontSize: 20, fontWeight: FontWeight.bold)), +// content: Text("No Car or Driver Found in your area.".tr, +// style: AppStyle.title.copyWith(fontSize: 16)), +// actions: [ +// CupertinoDialogAction( +// onPressed: () { +// Get.back(); +// Get.offAll(() => const MapPagePassenger()); +// }, +// child: +// Text('OK'.tr, style: TextStyle(color: AppColor.greenColor)), +// ), +// ], +// ), +// ), +// barrierDismissible: false, +// ); +// } + +// Future postRideDetailsToServer() async { +// // التأكد من وجود مسار +// if (polylineCoordinates.isEmpty) return false; + +// startLocation = polylineCoordinates.first; +// endLocation = polylineCoordinates.last; + +// // تجهيز البيانات الكاملة (Data Enrichment) لإرسالها للـ PHP +// Map payload = { +// // 1. البيانات الأساسية +// "start_location": '${startLocation.latitude},${startLocation.longitude}', +// "end_location": '${endLocation.latitude},${endLocation.longitude}', +// "date": DateTime.now().toString(), +// "time": DateTime.now().toString(), +// "endtime": "00:00:00", // أو حسب حساباتك +// "price": totalPassenger.toStringAsFixed(2), +// "passenger_id": box.read(BoxName.passengerID).toString(), +// "driver_id": "0", // لم يحدد بعد +// "status": "waiting", +// "carType": box.read(BoxName.carType), +// "price_for_driver": totalPassenger.toString(), // أو المعادلة الخاصة بك +// "price_for_passenger": totalME.toString(), +// "distance": distance.toString(), + +// // 2. بيانات الراكب (ليستخدمها PHP لبناء الـ Payload دون استعلام) +// "passenger_name": box.read(BoxName.name).toString(), +// "passenger_phone": box.read(BoxName.phone).toString(), +// "passenger_token": box.read(BoxName.tokenFCM).toString(), +// "passenger_email": box.read(BoxName.email).toString(), +// "passenger_wallet": box.read(BoxName.passengerWalletTotal).toString(), +// "passenger_rating": (passengerRate ?? 5.0).toString(), + +// // 3. بيانات الواجهة الإضافية +// "start_name": startNameAddress, +// "end_name": endNameAddress, +// "duration_text": "${(durationToRide / 60).floor()}", // نص الوقت +// "distance_text": "$distance", // نص المسافة +// "is_wallet": Get.find().isWalletChecked.toString(), +// "has_steps": Get.find().wayPoints.length > 1 +// ? 'true' +// : 'false', + +// // نقاط التوقف (إذا وجدت) +// "step0": placesCoordinate.length > 0 ? placesCoordinate[0] : "", +// "step1": placesCoordinate.length > 1 ? placesCoordinate[1] : "", +// "step2": placesCoordinate.length > 2 ? placesCoordinate[2] : "", +// "step3": placesCoordinate.length > 3 ? placesCoordinate[3] : "", +// "step4": placesCoordinate.length > 4 ? placesCoordinate[4] : "", +// }; +// Log.print( +// '🏁 Ride Registration Detail: $startNameAddress -> $endNameAddress'); +// Log.print(' 📦 Payload: $payload'); + +// try { +// // الاتصال بـ add_ride.php +// var response = await CRUD().post( +// link: "${AppLink.server}/ride/rides/add_ride.php", // تأكد من المسار +// payload: payload); + +// var jsonResponse = (response is String) ? jsonDecode(response) : response; + +// if (jsonResponse['status'] == 'success') { +// rideId = jsonResponse['message'].toString(); // حفظ ID الرحلة +// Log.print("✅ Ride Created ID: $rideId"); +// return true; +// } else { +// Log.print("❌ Ride Creation Failed: $response"); +// return false; +// } +// } catch (e) { +// Log.print("❌ Exception in postRide: $e"); +// return false; +// } +// } + +// late LatLng endLocation; +// late LatLng startLocation; + +// StreamController _rideStatusStreamController = +// StreamController.broadcast(); +// Stream get rideStatusStream => _rideStatusStreamController.stream; + +// int maxAttempts = 28; + +// Future rideAppliedFromDriver(bool isApplied) async { +// Log.print('[rideAppliedFromDriver] 🚀 Starting logic...'); + +// // 1. جلب بيانات السائق والسيارة المحدثة من السيرفر +// await getUpdatedRideForDriverApply(rideId); + +// // تنبيهات الأسعار حسب نوع السيارة +// if (['Speed', 'Awfar Car'].contains(box.read(BoxName.carType))) { +// NotificationController().showNotification('Fixed Price'.tr, +// 'The captain is responsible for the route.'.tr, 'ding'); +// } else if (['Comfort', 'Lady'].contains(box.read(BoxName.carType))) { +// NotificationController().showNotification('Attention'.tr, +// 'The price may increase if the route changes.'.tr, 'ding'); +// } + +// isApplied = true; +// statusRide = 'Apply'; +// rideConfirm = false; +// isSearchingWindow = false; +// _isDriverAppliedLogicExecuted = true; // ضمان عدم التكرار + +// update(); // تحديث أولي + +// // 2. جلب موقع السائق الأولي فوراً (Blocking await) +// await getDriverCarsLocationToPassengerAfterApplied(); + +// // 3. إذا توفر الموقع: حساب المسافة/الزمن ورسم المسار +// if (driverCarsLocationToPassengerAfterApplied.isNotEmpty) { +// LatLng driverPos = driverCarsLocationToPassengerAfterApplied.last; + +// Log.print( +// '[rideAppliedFromDriver] 📍 Driver at: $driverPos, Passenger at: $passengerLocation'); + +// // أ) استدعاء API لحساب المسافة والزمن الدقيق (بدون رسم) +// await getInitialDriverDistanceAndDuration(driverPos, passengerLocation); + +// // ب) رسم خط المسار (Visual only) +// await drawDriverPathOnly(driverPos, passengerLocation); + +// // ج) ضبط الكاميرا لتشمل السائق والراكب +// _fitCameraToPoints(driverPos, passengerLocation); +// } else { +// Log.print( +// '[rideAppliedFromDriver] ⚠️ Warning: Driver location not found yet.'); +// } + +// // 4. تشغيل تايمر التتبع المستمر (الذي سيقوم بتناقص الوقت الذي جلبناه من API) +// startTimerFromDriverToPassengerAfterApplied(); + +// // إغلاق الستريم القديم +// if (!_rideStatusStreamController.isClosed) +// _rideStatusStreamController.close(); +// } + +// /// دالة لجلب المسافة والزمن بين السائق والراكب عند قبول الطلب +// /// تستخدم API سريع (overview=false) +// Future getInitialDriverDistanceAndDuration( +// LatLng driverPos, LatLng passengerPos) async { +// final String apiUrl = 'https://routec.intaleq.xyz/route'; +// final String apiKey = Env.mapKeyOsm; + +// final String origin = '${driverPos.latitude},${driverPos.longitude}'; +// final String dest = '${passengerPos.latitude},${passengerPos.longitude}'; + +// // الرابط المطلوب: steps=false&overview=false (سريع جداً للبيانات فقط) +// final Uri uri = Uri.parse( +// '$apiUrl?origin=$origin&destination=$dest&steps=false&overview=false'); + +// try { +// Log.print('[InitialCalc] Fetching distance/duration from: $uri'); +// final response = await http.get(uri, headers: {'X-API-KEY': apiKey}); + +// if (response.statusCode == 200) { +// final data = jsonDecode(response.body); + +// if (data['status'] == 'ok') { +// // 1. استخراج الزمن (بالثواني) +// // نستخدم المعامل 1.5348 أو 1.4 حسب منطقك السابق لتقدير الوقت الواقعي +// double durationSecondsRaw = (data['duration_s'] as num).toDouble(); +// int finalDurationSeconds = (durationSecondsRaw * kDurationScalar) +// .toInt(); // kDurationScalar = 1.5348 + +// // 2. استخراج المسافة (بالأمتار) +// double distanceMeters = (data['distance_m'] as num).toDouble(); + +// // 3. تحديث المتغيرات في الكنترولر +// timeToPassengerFromDriverAfterApplied = finalDurationSeconds; +// remainingTimeToPassengerFromDriverAfterApplied = finalDurationSeconds; + +// distanceByPassenger = +// (distanceMeters).toStringAsFixed(0); // المسافة نصاً + +// // يمكنك أيضاً تحديث durationToPassenger إذا كنت تستخدمها +// durationToPassenger = finalDurationSeconds; + +// Log.print( +// '[InitialCalc] ✅ Success: Duration=${finalDurationSeconds}s, Distance=${distanceMeters}m'); +// update(); // تحديث الواجهة لعرض الوقت الجديد فوراً +// } +// } else { +// Log.print('[InitialCalc] ❌ API Error: ${response.statusCode}'); +// } +// } catch (e) { +// Log.print('[InitialCalc] 💥 Exception: $e'); +// } +// } + +// // دالة خفيفة وسريعة لرسم خط المسار فقط (بدون أسعار أو خطوات) +// Future drawDriverPathOnly(LatLng driverPos, LatLng passengerPos) async { +// final String apiUrl = 'https://routec.intaleq.xyz/route'; +// final String apiKey = Env.mapKeyOsm; + +// final String origin = '${driverPos.latitude},${driverPos.longitude}'; +// final String dest = '${passengerPos.latitude},${passengerPos.longitude}'; + +// // استخدام overview=full للدقة، و steps=false للسرعة +// final Uri uri = Uri.parse( +// '$apiUrl?origin=$origin&destination=$dest&steps=false&overview=full'); + +// try { +// final response = await http.get(uri, headers: {'X-API-KEY': apiKey}); + +// if (response.statusCode == 200) { +// final data = jsonDecode(response.body); + +// if (data['status'] == 'ok' && data['polyline'] != null) { +// final String pointsString = data['polyline']; + +// // فك التشفير +// List decodedPoints = +// await compute(decodePolylineIsolate, pointsString); + +// // إزالة خط مسار السائق القديم فقط +// polyLines = polyLines +// .where((p) => p.polylineId.value != 'driver_route') +// .toSet(); + +// // إضافة الخط الجديد +// polyLines = { +// ...polyLines, +// Polyline( +// polylineId: const PolylineId('driver_route'), +// points: decodedPoints, +// color: const Color(0xFF333333), // لون مميز لمسار السائق +// width: 5, +// ) +// }; + +// // لا تستدعي update هنا، سيتم استدعاؤها في الدالة الأب (getDriverCars...) لتقليل عدد التحديثات +// } +// } +// } catch (e) { +// Log.print('Error drawing driver path: $e'); +// } +// } + +// // دالة مساعدة لضبط الكاميرا +// void _fitCameraToPoints(LatLng p1, LatLng p2) async { +// if (mapController == null) return; + +// // 1. معالجة حالة النقاط المتطابقة (تمنع الكراش في Android) +// if (p1.latitude == p2.latitude && p1.longitude == p2.longitude) { +// try { +// mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 17)); +// } catch (e) { +// Log.print("Error animating to single point: $e"); +// } +// return; +// } + +// // 2. حساب الحدود +// double minLat = min(p1.latitude, p2.latitude); +// double maxLat = max(p1.latitude, p2.latitude); +// double minLng = min(p1.longitude, p2.longitude); +// double maxLng = max(p1.longitude, p2.longitude); + +// // 3. تقليل الهوامش لتجنب خطأ "View size too small" +// // نستخدم 50 بدلاً من 100 ليكون آمناً مع الخرائط الصغيرة +// double padding = 50.0; + +// try { +// await mapController?.animateCamera( +// CameraUpdate.newLatLngBounds( +// LatLngBounds( +// southwest: LatLng(minLat, minLng), +// northeast: LatLng(maxLat, maxLng), +// ), +// left: padding, +// top: padding, +// right: padding, +// bottom: padding, +// ), +// ); +// } catch (e) { +// Log.print("Error animating bounds (Map might be resizing): $e"); +// // محاولة بديلة آمنة: تحريك الكاميرا للمنتصف فقط دون Bounds +// try { +// LatLng center = LatLng((minLat + maxLat) / 2, (minLng + maxLng) / 2); +// mapController?.animateCamera(CameraUpdate.newLatLngZoom(center, 14)); +// } catch (e) { +// Log.print("Error: $e"); +// } +// } +// } + +// // Listening to the Stream +// void listenToRideStatusStream() { +// rideStatusStream.listen((rideStatus) { +// Log.print("Ride Status: $rideStatus"); +// // Handle updates based on the ride status +// }, onError: (error) { +// Log.print("Error in Ride Status Stream: $error"); +// // Handle stream errors +// }, onDone: () { +// Log.print("Ride status stream closed."); +// }); +// } + +// void start15SecondTimer(String rideId) { +// Timer(const Duration(seconds: 15), () { +// // delayAndFetchRideStatusForAllDriverAvailable(rideId); +// }); +// } + +// // Replaces void startTimer() +// Timer? +// _uiCountdownTimer; // Add this variable to your class to manage lifecycle + +// void startUiCountdown() { +// // Cancel any existing timer to avoid duplicates +// _uiCountdownTimer?.cancel(); + +// // Reset variables +// progress = 0; +// remainingTime = durationTimer; + +// _uiCountdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { +// // Logic from your loop, but non-blocking +// int i = timer.tick; // current tick + +// progress = i / durationTimer; +// remainingTime = durationTimer - i; + +// if (remainingTime <= 0) { +// timer.cancel(); // Stop this specific timer +// rideConfirm = false; + +// // Add the duration to the tracking time logic +// timeToPassengerFromDriverAfterApplied += durationToPassenger; + +// // Note: We do NOT call startTimerFromDriverToPassengerAfterApplied() here +// // because we already started it in rideAppliedFromDriver! + +// timerEnded(); // Call your existing completion logic +// } +// update(); // Update the UI progress bar +// }); +// } + +// void timerEnded() async { +// runEvery30SecondsUntilConditionMet(); +// isCancelRidePageShown = false; +// Log.print('isCancelRidePageShown: $isCancelRidePageShown'); +// update(); +// } + +// Future getRideStatus(String rideId) async { +// final response = await CRUD().get( +// link: "${AppLink.rideServerSide}/ride/rides/getRideStatus.php", +// payload: {'id': rideId}); +// Log.print(response); +// Log.print('2176'); +// return jsonDecode(response)['data']; +// } + +// late String driverCarModel, +// driverCarMake, +// driverLicensePlate, +// driverName = ''; +// Future getUpdatedRideForDriverApply(String rideId) async { +// // حماية مبدئية: إذا كان المعرف غير صالح لا تكمل +// if (rideId == 'yet' || rideId.isEmpty) return; + +// try { +// final res = await CRUD().get( +// link: "${AppLink.server}/ride/rides/getRideOrderID.php", +// payload: {'passengerID': box.read(BoxName.passengerID).toString()}); + +// if (res != 'failure') { +// var response = jsonDecode(res); +// Log.print('getUpdatedRideForDriverApply Response: $response'); + +// // [هام] التحقق من أن data عبارة عن Map وليست false أو null +// // هذا يمنع الخطأ: Class 'bool' has no instance method '[]' +// if (response['status'] == 'success' && +// response['data'] != null && +// response['data'] is Map) { +// var data = response['data']; + +// // استخدام ?.toString() ?? '' للحماية من القيم الفارغة (Null Safety) +// driverId = data['driver_id']?.toString() ?? ''; +// driverPhone = data['phone']?.toString() ?? ''; +// driverCarMake = data['make']?.toString() ?? ''; +// model = data['model']?.toString() ?? ''; +// colorHex = data['color_hex']?.toString() ?? ''; +// carColor = data['color']?.toString() ?? ''; +// make = data['make']?.toString() ?? ''; +// licensePlate = data['car_plate']?.toString() ?? ''; + +// // دمج الاسم الأول والأخير للراكب +// String firstName = data['passengerName']?.toString() ?? ''; +// String lastName = data['last_name']?.toString() ?? ''; +// passengerName = +// lastName.isNotEmpty ? "$firstName $lastName" : firstName; + +// driverName = data['driverName']?.toString() ?? ''; + +// // [هام] التوكن ضروري للإشعارات +// driverToken = data['token']?.toString() ?? ''; + +// carYear = data['year']?.toString() ?? ''; +// driverRate = data['ratingDriver']?.toString() ?? '5.0'; + +// update(); // تحديث الواجهة بالبيانات الجديدة +// } else { +// Log.print( +// "Warning: Ride data not found or invalid (data is false/null)"); +// // اختياري: يمكنك هنا التعامل مع حالة عدم العثور على السائق بعد +// } +// } +// } catch (e) { +// Log.print("Error in getUpdatedRideForDriverApply: $e"); +// } +// } + +// late LatLng currentDriverLocation; +// late double headingList; + +// Map _animationTimers = {}; +// final int updateIntervalMs = 100; // Update every 100ms +// final double minMovementThreshold = +// 10; // Minimum movement in meters to trigger update +// Future getCarForFirstConfirm(String carType) async { +// bool foundCars = false; +// int attempt = 0; + +// // Set up the periodic timer +// Timer? timer = Timer.periodic(const Duration(seconds: 4), (Timer t) async { +// // Attempt to get car location +// foundCars = await getCarsLocationByPassengerAndReloadMarker(); +// Log.print('foundCars: $foundCars'); + +// if (foundCars) { +// // If cars are found, cancel the timer and exit the search +// t.cancel(); +// } else if (attempt >= 4) { +// // After 4 attempts, stop the search +// t.cancel(); + +// if (!foundCars) { +// noCarString = true; +// dataCarsLocationByPassenger = 'failure'; +// } + +// update(); +// } + +// attempt++; // Increment attempt +// }); +// } + +// void startCarLocationSearch(String carType) { +// int searchInterval = 5; // Interval in seconds +// Log.print('searchInterval: $searchInterval'); +// int boundIncreaseStep = 2500; // Initial bounds in meters +// Log.print('boundIncreaseStep: $boundIncreaseStep'); +// int maxAttempts = 3; // Maximum attempts to increase bounds +// int maxBoundIncreaseStep = 6000; // Maximum bounds increase step +// int attempt = 0; // Current attempt +// Log.print('initial attempt: $attempt'); + +// Timer.periodic(Duration(seconds: searchInterval), (Timer timer) async { +// Log.print('Current attempt: $attempt'); // Log current attempt +// bool foundCars = false; +// if (attempt >= maxAttempts) { +// timer.cancel(); +// if (foundCars == false) { +// noCarString = true; +// // dataCarsLocationByPassenger = 'failure'; +// update(); +// } + +// // return; +// } else if (reloadStartApp == true) { +// Log.print('reloadStartApp: $reloadStartApp'); +// foundCars = await getCarsLocationByPassengerAndReloadMarker(); +// Log.print('foundCars: $foundCars'); + +// if (foundCars) { +// timer.cancel(); +// } else { +// attempt++; +// Log.print( +// 'Incrementing attempt to: $attempt'); // Log incremented attempt + +// if (boundIncreaseStep < maxBoundIncreaseStep) { +// boundIncreaseStep += 1500; // Increase bounds +// if (boundIncreaseStep > maxBoundIncreaseStep) { +// boundIncreaseStep = +// maxBoundIncreaseStep; // Ensure it does not exceed the maximum +// } +// Log.print( +// 'New boundIncreaseStep: $boundIncreaseStep'); // Log new bounds +// } +// } +// } +// }); +// } + +// String getLocationArea(double latitude, double longitude) { +// LatLng passengerPoint = LatLng(latitude, longitude); + +// // 1. فحص الأردن +// if (isPointInPolygon(passengerPoint, CountryPolygons.jordanBoundary)) { +// box.write(BoxName.countryCode, 'Jordan'); +// // يمكنك تعيين AppLink.endPoint هنا إذا كان منطقك الداخلي لا يزال يعتمد عليه +// box.write(BoxName.serverChosen, +// AppLink.server); // مثال: اختر سيرفر سوريا للبيانات +// return 'Jordan'; +// } + +// // 2. فحص سوريا +// if (isPointInPolygon(passengerPoint, CountryPolygons.syriaBoundary)) { +// box.write(BoxName.countryCode, 'Syria'); +// box.write(BoxName.serverChosen, AppLink.server); +// return 'Syria'; +// } + +// // 3. فحص مصر +// if (isPointInPolygon(passengerPoint, CountryPolygons.egyptBoundary)) { +// box.write(BoxName.countryCode, 'Egypt'); +// box.write(BoxName.serverChosen, AppLink.server); +// return 'Egypt'; +// } + +// // 4. الافتراضي (إذا كان خارج المناطق المخدومة) +// box.write(BoxName.countryCode, 'Jordan'); +// box.write(BoxName.serverChosen, AppLink.server); +// return 'Unknown Location (Defaulting to Jordan)'; +// } + +// Future getCarsLocationByPassengerAndReloadMarker() async { +// // 1. تنظيف القائمة والماركرز +// carsLocationByPassenger = []; + +// if (passengerLocation.latitude == 0 && passengerLocation.longitude == 0) { +// return false; // لا يوجد موقع للراكب +// } + +// // 2. طلب بسيط ومباشر (أنا هنا، أعطني السائقين حولي) +// var res = await CRUD().get( +// link: AppLink.getCarsLocationByPassenger, +// payload: { +// 'lat': passengerLocation.latitude.toString(), +// 'lng': passengerLocation.longitude.toString(), +// 'radius': '5', // نصف القطر ثابت (مثلاً 5 كم) أو يمكنك جعله ديناميكياً +// 'limit': '50', // أقصى عدد سيارات للعرض +// }, +// ); + +// if (res == 'failure') { +// noCarString = true; +// update(); +// return false; +// } + +// // 3. معالجة البيانات +// noCarString = false; +// var responseData = jsonDecode(res); + +// // دعم التنسيقين (data أو message) لضمان عدم حدوث كراش +// List driversList = []; +// if (responseData['status'] == true && responseData['data'] != null) { +// driversList = responseData['data']; +// } else if (responseData['message'] != null) { +// driversList = responseData['message']; // للكود القديم احتياطاً +// } + +// if (driversList.isEmpty) { +// carsLocationByPassenger.clear(); +// update(); +// return false; +// } + +// carsLocationByPassenger.clear(); // تنظيف الماركرز القديمة + +// // 4. رسم السيارات على الخريطة +// for (var i = 0; i < driversList.length; i++) { +// var carData = driversList[i]; + +// // التحقق من الإحداثيات لضمان عدم رسم سيارة في المحيط +// double lat = double.tryParse(carData['latitude'].toString()) ?? 0.0; +// double lng = double.tryParse(carData['longitude'].toString()) ?? 0.0; +// double heading = double.tryParse(carData['heading'].toString()) ?? 0.0; + +// if (lat == 0.0 || lng == 0.0) continue; + +// _updateOrCreateMarker( +// carData['id'].toString(), +// LatLng(lat, lng), +// heading, +// // الدالة هذه تقرر شكل الأيقونة بناءً على نوع السيارة القادم من السيرفر +// _getIconForCar(carData), +// ); +// } + +// update(); +// return true; +// } + +// final List> fakeCarData = []; + +// void _addFakeCarMarkers(LatLng center, int count) { +// if (fakeCarData.isEmpty) { +// Random random = Random(); +// double radiusInKm = 2.5; // 3 km diameter, so 1.5 km radius + +// for (int i = 0; i < count; i++) { +// // Generate a random angle and distance within the circle +// double angle = random.nextDouble() * 2 * pi; +// double distance = sqrt(random.nextDouble()) * radiusInKm; + +// // Convert distance to latitude and longitude offsets +// double latOffset = (distance / 111.32); // 1 degree lat ≈ 111.32 km +// double lonOffset = +// (distance / (111.32 * cos(radians(center.latitude)))); + +// // Calculate new position +// double lat = center.latitude + (latOffset * cos(angle)); +// double lon = center.longitude + (lonOffset * sin(angle)); + +// double heading = random.nextDouble() * 360; + +// fakeCarData.add({ +// 'id': 'fake_$i', +// 'latitude': lat, +// 'longitude': lon, +// 'heading': heading, +// 'gender': 'Male', // Randomize gender +// }); +// } +// } + +// for (var carData in fakeCarData) { +// _updateOrCreateMarker( +// carData['id'].toString(), +// LatLng(carData['latitude'], carData['longitude']), +// carData['heading'], +// _getIconForCar(carData), +// ); +// } +// } + +// String _getIconForCar(Map carData) { +// if (carData['model'].toString().contains('دراجة')) { +// return motoIcon; +// } else if (carData['gender'] == 'Female') { +// return ladyIcon; +// } else { +// return carIcon; +// } +// } + +// void _updateOrCreateMarker( +// String markerId, LatLng newPosition, double newHeading, String icon) { +// final mId = MarkerId(markerId); +// final existingMarker = markers.cast().firstWhere( +// (m) => m?.markerId == mId, +// orElse: () => null, +// ); + +// if (existingMarker == null) { +// markers = { +// ...markers, +// Marker( +// markerId: mId, +// position: newPosition, +// rotation: newHeading, +// icon: InlqBitmap.fromStyleImage(icon), +// anchor: const Offset(0.5, 0.5), +// ), +// }; +// update(); +// } else { +// double distance = +// _calculateDistance(existingMarker.position, newPosition); +// if (distance >= minMovementThreshold) { +// _smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon); +// } +// } +// } + +// double _calculateDistance(LatLng start, LatLng end) { +// // Implement distance calculation (e.g., Haversine formula) +// // For simplicity, this is a placeholder. Replace with actual implementation. +// return 1000 * +// sqrt(pow(start.latitude - end.latitude, 2) + +// pow(start.longitude - end.longitude, 2)); +// } + +// String formatSyrianPhoneNumber(String phoneNumber) { +// // Trim any whitespace from the input. +// String trimmedPhone = phoneNumber.trim(); + +// // If the number starts with '09', remove the leading '0' and prepend '963'. +// if (trimmedPhone.startsWith('09')) { +// return '963${trimmedPhone.substring(1)}'; +// } +// // If the number already starts with '963', return it as is to avoid duplication. +// if (trimmedPhone.startsWith('963')) { +// return trimmedPhone; +// } +// // For any other case (e.g., number starts with '9' without a '0'), +// // prepend '963' to ensure the correct format. +// return '963$trimmedPhone'; +// } + +// String generateTrackingLink(String rideId, String driverId) { +// String cleanRideId = rideId.toString().trim(); +// String cleanDriverId = driverId.toString().trim(); + +// // الكلمة السرية للمطابقة مع السيرفر +// const String secretSalt = "Intaleq_Secure_Track_2025"; + +// // الدمج والتشفير +// String rawString = "$cleanRideId$cleanDriverId$secretSalt"; +// var bytes = utf8.encode(rawString); +// var digest = md5.convert(bytes); +// String token = digest.toString(); + +// // الرابط المباشر لصفحة التتبع +// return "https://intaleqapp.com/track/index.php?id=$cleanRideId&token=$token"; +// } + +// // 2. الدالة الرئيسية (تم تعديلها لإرسال واتساب بدلاً من الإشعارات) +// Future shareTripWithFamily() async { +// // التحقق أولاً: هل الرقم موجود؟ +// String? storedPhone = box.read(BoxName.sosPhonePassenger); + +// if (storedPhone == null) { +// // --- (نفس المنطق القديم: فتح ديالوج لإضافة الرقم) --- +// Get.defaultDialog( +// title: 'Add SOS Phone'.tr, +// titleStyle: AppStyle.title, +// content: Form( +// key: sosFormKey, +// child: MyTextForm( +// controller: sosPhonePassengerProfile, +// label: 'insert sos phone'.tr, +// hint: 'e.g. 0912345678'.tr, +// type: TextInputType.phone, +// ), +// ), +// confirm: MyElevatedButton( +// title: 'Add SOS Phone'.tr, +// onPressed: () async { +// if (sosFormKey.currentState!.validate()) { +// Get.back(); +// // تنسيق الرقم +// var numberPhone = +// formatSyrianPhoneNumber(sosPhonePassengerProfile.text); + +// // حفظ في السيرفر +// await CRUD().post( +// link: AppLink.updateprofile, +// payload: { +// 'id': box.read(BoxName.passengerID), +// 'sosPhone': numberPhone, +// }, +// ); + +// // حفظ محلياً +// box.write(BoxName.sosPhonePassenger, numberPhone); + +// // استدعاء الدالة مرة أخرى للمتابعة +// shareTripWithFamily(); +// } +// })); +// return; +// } + +// // --- (المنطق الجديد: إرسال واتساب مباشرة) --- + +// // 1. التأكد من وجود بيانات للرحلة +// if (rideId == 'yet' || driverId.isEmpty) { +// Get.snackbar("Alert".tr, "Wait for the trip to start first".tr); +// return; +// } + +// // 2. تنسيق الرقم +// var numberPhone = formatSyrianPhoneNumber(storedPhone); + +// // 3. توليد الرابط +// String trackingLink = generateTrackingLink(rideId, driverId); + +// // 4. تجهيز الرسالة (بالإنجليزية وجاهزة للترجمة) +// // لاحظ: استخدمت المتغيرات الموجودة في الكنترولر (passengerName هنا عادة يحمل اسم السائق في الكنترولر الخاص بك حسب الكود السابق) +// String message = """ +// مرحباً، تابع رحلتي مباشرة على تطبيق انطلق 🚗 + +// يمكنك تتبع مسار الرحلة من هنا: +// $trackingLink + +// السائق: $passengerName +// السيارة: $model - $licensePlate +// شكراً لاستخدامك انطلق! +// """ +// .tr; + +// String messageEn = """Hello, follow my trip live on Intaleq 🚗 + +// Track my ride here: +// $trackingLink + +// Driver: $passengerName +// Car: $model - $licensePlate +// Thank you for using Intaleq! +// """; + +// // اختر الرسالة بناءً على اللغة المفضلة (مثال بسيط) +// String userLanguage = box.read(BoxName.lang) ?? 'ar'; +// message = (userLanguage == 'ar') ? message : messageEn; +// // وضعنا .tr لكي تتمكن من ترجمتها للعربية في ملفات اللغة إذا أردت، أو تركها إنجليزية + +// Log.print("Sending WhatsApp to: $numberPhone"); + +// // 5. فتح واتساب +// launchCommunication('whatsapp', numberPhone, message); + +// // (اختياري) حفظ أن التتبع مفعل لتغيير حالة الأيقونة في الواجهة +// box.write(BoxName.parentTripSelected, true); +// update(); +// } + +// Future getTokenForParent() async { +// // 1. التحقق أولاً: هل الرقم موجود؟ +// String? storedPhone = box.read(BoxName.sosPhonePassenger); + +// if (storedPhone == null) { +// // --- حالة الرقم غير موجود: نفتح الديالوج فقط --- +// Get.defaultDialog( +// title: 'Add SOS Phone'.tr, +// titleStyle: AppStyle.title, +// content: Form( +// key: sosFormKey, +// child: MyTextForm( +// controller: sosPhonePassengerProfile, +// label: 'insert sos phone'.tr, +// hint: 'e.g. 0912345678'.tr, +// type: TextInputType.phone, +// ), +// ), +// confirm: MyElevatedButton( +// title: 'Add SOS Phone'.tr, +// onPressed: () async { +// if (sosFormKey.currentState!.validate()) { +// // إغلاق الديالوج الحالي +// Get.back(); + +// // تنسيق الرقم (تأكد أن هذا التنسيق يطابق ما تم تخزينه عند تسجيل الراكب) +// var numberPhone = +// formatSyrianPhoneNumber(sosPhonePassengerProfile.text); + +// // حفظ الرقم في السيرفر (تحديث البروفايل) +// await CRUD().post( +// link: AppLink.updateprofile, +// payload: { +// 'id': box.read(BoxName.passengerID), +// 'sosPhone': numberPhone, +// }, +// ); + +// // حفظ الرقم محلياً +// box.write(BoxName.sosPhonePassenger, numberPhone); + +// // استدعاء الدالة مرة أخرى +// getTokenForParent(); +// } +// })); +// return; +// } +// generateTrackingLink(rideId, driverId); +// // --- حالة الرقم موجود: نكمل التنفيذ --- +// var numberPhone = formatSyrianPhoneNumber(storedPhone); +// Log.print("Searching for Parent Token with Phone: $numberPhone"); + +// // استدعاء السكريبت (استخدم POST بدلاً من GET) +// var res = await CRUD() +// .post(link: AppLink.getTokenParent, payload: {'phone': numberPhone}); + +// // التعامل مع الاستجابة +// if (res is Map) { +// handleResponse(res); +// } else { +// try { +// // var jsonRes = jsonDecode(res); +// handleResponse(res); +// } catch (e) { +// Log.print("Error parsing response: $res"); +// } +// } +// } + +// void handleResponse(Map res) { +// Log.print("Handle Response: $res"); // للتأكد من دخول الدالة + +// // الحالة 1: الرقم غير مسجل (Failure) +// if (res['status'] == 'failure') { +// // إذا كان هناك أي ديالوج تحميل مفتوح، نغلقه أولاً، لكن بحذر +// if (Get.isDialogOpen ?? false) Get.back(); + +// Get.defaultDialog( +// title: "No user found".tr, // اختصرت العنوان ليظهر بشكل أفضل +// titleStyle: AppStyle.title, +// content: Column( +// children: [ +// Text( +// "No passenger found for the given phone number".tr, +// style: AppStyle.title, // غيرت الستايل ليكون أصغر قليلاً +// textAlign: TextAlign.center, +// ), +// const SizedBox(height: 10), +// Text( +// "Send Intaleq app to him".tr, +// style: AppStyle.title +// .copyWith(color: AppColor.greenColor, fontSize: 14), +// textAlign: TextAlign.center, +// ) +// ], +// ), +// confirm: MyElevatedButton( +// title: 'Send Invite'.tr, +// onPressed: () { +// Get.back(); // إغلاق الديالوج + +// var rawPhone = box.read(BoxName.sosPhonePassenger); +// // تأكد أن rawPhone ليس null +// if (rawPhone == null) return; + +// var phone = formatSyrianPhoneNumber(rawPhone); + +// // تصحيح نص الرسالة +// var message = '''Dear Friend, + +// 🚀 I have just started an exciting trip on Intaleq! +// Download the app to track my ride: + +// 👉 Android: https://play.google.com/store/apps/details?id=com.Intaleq.intaleq&hl=en-US +// 👉 iOS: https://apps.apple.com/st/app/intaleq-rider/id6748075179 + +// See you there! +// Intaleq Team'''; + +// launchCommunication('whatsapp', phone, message); +// }), +// cancel: MyElevatedButton( +// title: 'Cancel'.tr, +// onPressed: () { +// Get.back(); +// })); +// } +// // الحالة 2: نجاح (Success) +// else if (res['status'] == 'success') { +// // إغلاق أي ديالوج سابق (مثل Loading) +// if (Get.isDialogOpen ?? false) Get.back(); + +// Get.snackbar("Success".tr, "The invitation was sent successfully".tr, +// backgroundColor: AppColor.greenColor, colorText: Colors.white); + +// List tokensData = res['data']; + +// for (var device in tokensData) { +// String tokenParent = device['token']; + +// NotificationService.sendNotification( +// category: "Trip Monitoring", +// target: tokenParent, +// title: "Trip Monitoring".tr, +// body: "Click to track the trip".tr, +// isTopic: false, +// tone: 'tone1', +// driverList: [rideId, driverId], +// ); +// // حفظ آخر توكن +// box.write(BoxName.tokenParent, tokenParent); +// } +// box.write(BoxName.parentTripSelected, true); +// } +// } + +// // Function to check if the point is inside the polygon +// bool isPointInPolygon(LatLng point, List polygon) { +// int intersections = 0; +// for (int i = 0; i < polygon.length; i++) { +// LatLng vertex1 = polygon[i]; +// LatLng vertex2 = +// polygon[(i + 1) % polygon.length]; // Loop back to the start + +// if (_rayIntersectsSegment(point, vertex1, vertex2)) { +// intersections++; +// } +// } + +// // If the number of intersections is odd, the point is inside +// return intersections % 2 != 0; +// } + +// // Helper function to check if a ray from the point intersects with a polygon segment +// bool _rayIntersectsSegment(LatLng point, LatLng vertex1, LatLng vertex2) { +// double px = point.longitude; +// double py = point.latitude; + +// double v1x = vertex1.longitude; +// double v1y = vertex1.latitude; +// double v2x = vertex2.longitude; +// double v2y = vertex2.latitude; + +// // Check if the point is outside the vertical bounds of the segment +// if ((py < v1y && py < v2y) || (py > v1y && py > v2y)) { +// return false; +// } + +// // Calculate the intersection of the ray and the segment +// double intersectX = v1x + (py - v1y) * (v2x - v1x) / (v2y - v1y); + +// // Check if the intersection is to the right of the point +// return intersectX > px; +// } + +// bool isInUniversity = false; +// // Function to check if the passenger is in any university polygon +// // Function to check if the passenger is in any university polygon and return the university name +// String checkPassengerLocation(LatLng passengerLocation, +// List> universityPolygons, List universityNames) { +// for (int i = 0; i < universityPolygons.length; i++) { +// if (isPointInPolygon(passengerLocation, universityPolygons[i])) { +// isInUniversity = true; +// return "Passenger is in ${universityNames[i]}"; +// } +// } +// return "Passenger is not in any university"; +// } + +// String passengerLocationStringUnvirsity = 'unKnown'; +// void getPassengerLocationUniversity() { +// // Check if the passenger is inside any of the university polygons and get the university name +// passengerLocationStringUnvirsity = checkPassengerLocation( +// passengerLocation, +// UniversitiesPolygons.universityPolygons, +// UniversitiesPolygons.universityNames, +// ); +// if (passengerLocationStringUnvirsity != 'unKnown') { +// // Get.snackbar('you are in $passengerLocationStringUnvirsity', ""); +// } +// Log.print(passengerLocationStringUnvirsity); +// } + +// // Initialize polygons from UniversitiesPolygons +// void _initializePolygons() { +// List> universityPolygons = +// UniversitiesPolygons.universityPolygons; + +// for (int i = 0; i < universityPolygons.length; i++) { +// Polygon polygon = Polygon( +// polygonId: PolygonId('univ_$i'), +// points: universityPolygons[i], +// fillColor: Colors.blueAccent.withOpacity(0.2), +// strokeColor: Colors.blueAccent, +// strokeWidth: 2, +// ); +// polygons.add(polygon); +// } +// update(); +// } + +// LatLng driverLocationToPassenger = const LatLng(32, 35); +// Future getDriverCarsLocationToPassengerAfterApplied() async { +// // driverCarsLocationToPassengerAfterApplied +// // 1. الشرط الأمني: تتبع فقط إذا كانت الرحلة نشطة +// bool isRideActive = (statusRide == 'Apply' || +// statusRide == 'Arrived' || +// statusRide == 'Begin' || +// currentRideState.value == RideState.driverApplied || +// currentRideState.value == RideState.driverArrived || +// currentRideState.value == RideState.inProgress); + +// if (!isRideActive || +// statusRide == 'Finished' || +// statusRide == 'Cancel' || +// currentRideState.value == RideState.finished || +// currentRideState.value == RideState.noRide || +// currentRideState.value == RideState.preCheckReview) { +// return; +// } + +// // 2. منع التداخل (Blocking) +// if (_isFetchingDriverLocation) return; +// _isFetchingDriverLocation = true; + +// try { +// var res = await CRUD().get( +// link: AppLink.getDriverCarsLocationToPassengerAfterApplied, +// payload: {'driver_id': driverId}); + +// if (res != 'failure') { +// datadriverCarsLocationToPassengerAfterApplied = jsonDecode(res); + +// if (datadriverCarsLocationToPassengerAfterApplied['message'] != null && +// datadriverCarsLocationToPassengerAfterApplied['message'] +// .isNotEmpty) { +// var _data = +// datadriverCarsLocationToPassengerAfterApplied['message'][0]; + +// LatLng newDriverPos = LatLng( +// double.parse(_data['latitude'].toString()), +// double.parse(_data['longitude'].toString())); +// // أضف هذا السطر لتقليل استهلاك الذاكرة +// if (driverCarsLocationToPassengerAfterApplied.length > 10) { +// driverCarsLocationToPassengerAfterApplied.removeAt(0); +// } +// driverLocationToPassenger = newDriverPos; +// driverCarsLocationToPassengerAfterApplied.add(newDriverPos); +// // 🔥 الإضافة هنا أيضاً 🔥 +// // 🔥 تحديث التوقيت حتى لو جاءت من API لكي يهدأ الحارس قليلاً +// _lastSocketLocationTime = DateTime.now(); +// _checkAndRecalculateIfDeviated(newDriverPos); +// // [تعديل هام] تنظيف آمن: لا نحذف ماركر السائق الحالي +// clearMarkersExceptStartEndAndDriver(); + +// // تحريك الماركر +// reloadMarkerDriverCarsLocationToPassengerAfterApplied(); +// } +// } +// update(); +// } catch (e) { +// Log.print('Error fetching driver location: $e'); +// } finally { +// _isFetchingDriverLocation = false; +// } +// } + +// Future runEvery30SecondsUntilConditionMet() async { +// // Calculate the duration of the trip in minutes. +// double tripDurationInMinutes = durationToPassenger / 5; +// int loopCount = tripDurationInMinutes.ceil(); +// // If the trip duration is less than or equal to 50 minutes, then break the loop. +// for (var i = 0; i < loopCount; i++) { +// // Wait for 50 seconds. +// await Future.delayed(const Duration(seconds: 5)); +// if (rideTimerBegin == true || statusRide == 'Apply') { +// await getDriverCarsLocationToPassengerAfterApplied(); +// reloadMarkerDriverCarsLocationToPassengerAfterApplied(); +// } +// } +// } + +// Future runWhenRideIsBegin() async { +// // Calculate the duration of the trip in minutes. +// double tripDurationInMinutes = durationToRide / 6; +// int loopCount = tripDurationInMinutes.ceil(); +// // If the trip duration is less than or equal to 50 minutes, then break the loop. +// clearMarkersExceptStartEnd(); +// for (var i = 0; i < loopCount; i++) { +// // Wait for 50 seconds. +// await Future.delayed(const Duration(seconds: 4)); +// // if (rideTimerBegin == true && statusRide == 'Apply') { +// await getDriverCarsLocationToPassengerAfterApplied(); +// // } +// reloadMarkerDriverCarsLocationToPassengerAfterApplied(); +// } +// } + +// Timer? _timer; +// // final int updateIntervalMs = 100; // Update every 100ms +// // final double minMovementThreshold = +// // 1.0; // Minimum movement in meters to trigger update +// void clearMarkersExceptStartEndAndDriver() { +// markers.removeWhere((marker) { +// String id = marker.markerId.value; +// // لا تحذف نقطة البداية +// if (id == 'start') return false; +// // لا تحذف نقطة النهاية +// if (id == 'end') return false; +// // لا تحذف السائق الحالي +// if (id == currentDriverMarkerId) return false; + +// // احذف أي شيء آخر (مثل السيارات التي ظهرت وقت البحث) +// return true; +// }); + +// // ملاحظة: لا نستدعي update() هنا لأننا سنستدعيها في نهاية الدالة الرئيسية +// } + +// void clearMarkersExceptStartEnd() { +// markers.removeWhere((marker) { +// String id = marker.markerId.value; +// return id != 'start' && id != 'end'; +// }); + +// update(); +// } + +// // 1. تعريف ID ثابت للسائق طوال الرحلة +// String get currentDriverMarkerId => 'driver_marker_$driverId'; + +// void reloadMarkerDriverCarsLocationToPassengerAfterApplied() { +// if (datadriverCarsLocationToPassengerAfterApplied == null || +// datadriverCarsLocationToPassengerAfterApplied['message'] == null || +// datadriverCarsLocationToPassengerAfterApplied['message'].isEmpty) { +// return; +// } + +// var driverData = +// datadriverCarsLocationToPassengerAfterApplied['message'][0]; + +// // جلب الإحداثيات الجديدة +// LatLng newPosition = LatLng(double.parse(driverData['latitude'].toString()), +// double.parse(driverData['longitude'].toString())); + +// double newHeading = +// double.tryParse(driverData['heading'].toString()) ?? 0.0; + +// // تحديد الأيقونة +// String icon; +// if (driverData['model'].toString().contains('دراجة') || +// driverData['make'].toString().contains('دراجة')) { +// icon = motoIcon; +// } else if (driverData['gender'] == 'Female') { +// icon = ladyIcon; +// } else { +// icon = carIcon; +// } + +// // 2. البحث عن الماركر الجديد وتحديثه أو إنشاء جديد +// final String markerId = currentDriverMarkerId; +// final mId = MarkerId(markerId); +// final existingMarker = markers.cast().firstWhere( +// (m) => m?.markerId == mId, +// orElse: () => null, +// ); + +// if (existingMarker != null) { +// _smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon); +// } else { +// markers = { +// ...markers, +// Marker( +// markerId: mId, +// position: newPosition, +// rotation: newHeading, +// icon: InlqBitmap.fromStyleImage(icon), +// anchor: const Offset(0.5, 0.5), +// ), +// }; +// update(); +// } +// } + +// // التأكد من دالة التحريك السلس +// void _smoothlyUpdateMarker( +// Marker oldMarker, LatLng newPosition, double newHeading, String icon) { +// double distance = Geolocator.distanceBetween( +// oldMarker.position.latitude, +// oldMarker.position.longitude, +// newPosition.latitude, +// newPosition.longitude); + +// if (distance < 2.0) return; + +// final MarkerId markerIdKey = oldMarker.markerId; + +// _animationTimers[markerIdKey.value]?.cancel(); + +// int ticks = 0; +// const int totalSteps = 20; +// const int stepDuration = 50; + +// double latStep = +// (newPosition.latitude - oldMarker.position.latitude) / totalSteps; +// double lngStep = +// (newPosition.longitude - oldMarker.position.longitude) / totalSteps; +// double headingStep = (newHeading - oldMarker.rotation) / totalSteps; + +// LatLng currentPos = oldMarker.position; +// double currentHeading = oldMarker.rotation; + +// _animationTimers[markerIdKey.value] = +// Timer.periodic(const Duration(milliseconds: stepDuration), (timer) { +// ticks++; + +// currentPos = +// LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep); +// currentHeading += headingStep; + +// // Update the marker in the set +// final updatedMarker = oldMarker.copyWith( +// position: currentPos, +// rotation: currentHeading, +// icon: InlqBitmap.fromStyleImage(icon), +// ); + +// markers = { +// ...markers.where((m) => m.markerId != markerIdKey), +// updatedMarker, +// }; + +// // Native update through controller to avoid UI rebuild +// if (mapController != null) { +// mapController!.animateCamera(CameraUpdate.newLatLng( +// currentPos)); // Optional: Follow car if needed +// // Note: IntaleqMapController doesn't expose raw symbol update yet for Marker object, +// // but declarative update via GetBuilder is fast. +// } + +// update(); + +// if (ticks >= totalSteps) { +// timer.cancel(); +// _animationTimers.remove(markerIdKey.value); +// } +// }); +// } + +// void _updateMarkerPosition( +// LatLng newPosition, double newHeading, String icon) { +// const String markerId = 'driverToPassengers'; + +// final mId = MarkerId(markerId); +// final existingMarker = markers.cast().firstWhere( +// (m) => m?.markerId == mId, +// orElse: () => null, +// ); + +// if (existingMarker != null) { +// _smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon); +// } else { +// markers = { +// ...markers, +// Marker( +// markerId: mId, +// position: newPosition, +// rotation: newHeading, +// icon: InlqBitmap.fromStyleImage(icon), +// anchor: const Offset(0.5, 0.5), +// ), +// }; +// update(); +// } + +// mapController?.animateCamera(CameraUpdate.newLatLng(newPosition)); +// } + +// @override +// void onClose() { +// Log.print( +// "--- MapPassengerController: Closing and cleaning up all resources. ---"); + +// // 1. إلغاء المؤقتات الفردية (باستخدام ?. الآمن) + +// timerToPassengerFromDriverAfterApplied?.cancel(); +// _timer?.cancel(); +// _masterTimer?.cancel(); // (أضف المؤقت الرئيسي) +// _camThrottle?.cancel(); // (أضف مؤقت الكاميرا) +// _heartbeatTimer?.cancel(); +// EmergencySignalService.instance.stopListening(); +// if (isSocketConnected) { +// socket.emit('unsubscribe_all', +// {'passenger_id': box.read(BoxName.passengerID).toString()}); +// socket.disconnect(); +// socket.dispose(); +// } + +// // 2. إلغاء جميع المؤقتات في الخريطة (للتحريكات السلسة) +// _animationTimers.forEach((key, timer) { +// timer.cancel(); +// }); +// _animationTimers.clear(); + +// // 3. إغلاق متحكمات البث (StreamControllers) لمنع تسريب الذاكرة +// if (!_timerStreamController.isClosed) { +// _timerStreamController.close(); +// } +// if (!_beginRideStreamController.isClosed) { +// _beginRideStreamController.close(); +// } +// if (!_rideStatusStreamController.isClosed) { +// _rideStatusStreamController.close(); +// } +// if (!timerController.isClosed) { +// timerController.close(); +// } + +// // 4. التخلص من متحكم الخريطة (ممارسة جيدة) +// mapController = null; + +// Log.print("--- Cleanup complete. ---"); +// super.onClose(); +// } + +// restCounter() { +// clearPlacesDestination(); +// clearPolyline(); +// data = []; +// rideConfirm = false; +// shouldFetch = false; +// timeToPassengerFromDriverAfterApplied = 0; +// update(); +// } + +// //driver behaviour +// double calculateBearing(double lat1, double lon1, double lat2, double lon2) { +// double deltaLon = lon2 - lon1; +// double y = sin(deltaLon) * cos(lat2); +// double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(deltaLon); +// double bearing = atan2(y, x); +// return (bearing * 180 / pi + 360) % 360; // تحويل إلى درجات +// } + +// void analyzeBehavior(Position currentPosition, List routePoints) { +// double actualBearing = currentPosition.heading; // الاتجاه الفعلي من GPS +// double expectedBearing = calculateBearing( +// routePoints[0].latitude, +// routePoints[0].longitude, +// routePoints[1].latitude, +// routePoints[1].longitude, +// ); + +// double bearingDifference = (expectedBearing - actualBearing).abs(); +// if (bearingDifference > 30) { +// Log.print("⚠️ السائق انحرف عن المسار!"); +// } +// } + +// void detectStops(Position currentPosition) { +// if (currentPosition.speed < 0.5) { +// Log.print("🚦 السائق توقف في موقع غير متوقع!"); +// } +// } + +// Future cancelRideAfterRejectFromAll() async { +// clearPlacesDestination(); +// clearPolyline(); +// data = []; +// await CRUD().post( +// link: "${AppLink.server}/ride/rides/cancel_ride_by_passenger.php", +// payload: { +// "ride_id": rideId.toString(), // Convert to String +// "reason": 'notApplyFromAnyDriver' +// }); + +// rideConfirm = false; +// statusRide == 'Cancel'; +// isSearchingWindow = false; +// shouldFetch = false; +// isPassengerChosen = false; +// isCashConfirmPageShown = false; +// // totalStepDurations = 0; +// isCashSelectedBeforeConfirmRide = false; +// timeToPassengerFromDriverAfterApplied = 0; +// changeCancelRidePageShow(); +// remainingTime = 0; + +// update(); +// } + +// // متغيرات أسباب الإلغاء +// int selectedReasonIndex = -1; +// String selectedReasonText = ""; +// TextEditingController otherReasonController = TextEditingController(); + +// /// تحديث السبب المختار +// void selectReason(int index, String reason) { +// selectedReasonIndex = index; +// selectedReasonText = reason; +// update(); +// } + +// /// **دالة إلغاء الرحلة (النهائية)** +// Future cancelRide() async { +// // 1. التحقق من اختيار سبب +// if (selectedReasonIndex == -1) { +// Get.snackbar( +// 'Attention'.tr, +// 'Please select a reason first'.tr, +// snackPosition: SnackPosition.BOTTOM, +// backgroundColor: Colors.orange, +// colorText: Colors.white, +// ); +// return; +// } + +// // 2. تجهيز نص السبب النهائي +// String finalReason = selectedReasonText; +// if (finalReason == "Other".tr) { +// if (otherReasonController.text.trim().isEmpty) { +// Get.snackbar("Attention".tr, "Please write the reason...".tr, +// backgroundColor: Colors.red, colorText: Colors.white); +// return; +// } +// finalReason = otherReasonController.text.trim(); +// } + +// // 3. التنظيف المحلي الفوري (UX Optimization) +// Get.back(); // إغلاق الـ BottomSheet +// if (isCancelRidePageShown) +// changeCancelRidePageShow(); // إخفاء زر الإلغاء إن وجد + +// // 🔥 استدعاء دالة التنظيف الشاملة هنا 🔥 +// resetAllMapStates(); + +// // إيقاف جميع التايمرات +// // إيقاف جميع التايمرات +// stopAllTimers(); +// currentRideState.value = RideState.cancelled; +// await RideLiveNotification.cancel(); // إغلاق أندرويد +// IosLiveActivityService.endRideActivity(); // ✅ إغلاق iOS +// PipService.disablePip(); // ✅ إيقاف PiP عند الإلغاء + +// // 4. الاتصال بالسيرفر لإلغاء الرحلة وإبلاغ السائق +// if (rideId != 'yet' && rideId != null) { +// Log.print( +// '📡 Sending Cancel Request to Server with Reason: $finalReason'); + +// try { +// await CRUD().post( +// link: "${AppLink.server}/ride/rides/cancel_ride_by_passenger.php", +// payload: { +// "ride_id": rideId.toString(), +// "reason": finalReason // ✅ إرسال السبب للسيرفر +// }, +// ); +// // لا داعي لإرسال FCM أو Socket يدوياً من هنا، PHP يقوم بذلك +// } catch (e) { +// Log.print("Error cancelling on server: $e"); +// } +// } + +// // 5. العودة للصفحة الرئيسية +// Get.offAll(() => const MapPagePassenger()); +// } + +// void changePickerShown() { +// isPickerShown = !isPickerShown; +// heightPickerContainer = isPickerShown == true ? 150 : 90; +// update(); +// } + +// // ── Multi-Waypoint Methods ────────────────────────────────────────────────── +// void addMenuWaypoint() { +// if (activeMenuWaypointCount >= 2) return; +// activeMenuWaypointCount++; +// // Increase expanded bottom menu height to accommodate new waypoint row +// mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56); +// update(); +// } + +// void removeMenuWaypoint(int index) { +// if (index < 0 || index >= 2) return; +// // Shift items if removing first waypoint while second exists +// if (index == 0 && activeMenuWaypointCount == 2) { +// menuWaypoints[0] = menuWaypoints[1]; +// menuWaypointNames[0] = menuWaypointNames[1]; +// } +// menuWaypoints[activeMenuWaypointCount - 1] = null; +// menuWaypointNames[activeMenuWaypointCount - 1] = ''; +// activeMenuWaypointCount--; +// mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56); +// update(); +// } + +// void clearAllMenuWaypoints() { +// menuWaypoints = [null, null]; +// menuWaypointNames = ['', '']; +// activeMenuWaypointCount = 0; +// isPickingWaypoint = false; +// pickingWaypointIndex = -1; +// update(); +// } + +// void startPickingWaypointOnMap(int index) { +// pickingWaypointIndex = index; +// isPickingWaypoint = true; +// isPickerShown = true; +// heightPickerContainer = 150; +// // Close the expanded menu to show the map picker +// isMainBottomMenuMap = true; +// mainBottomMenuMapHeight = Get.height * .22; +// update(); +// } + +// void setMenuWaypointFromMap(int index, LatLng position) { +// Log.print('📍 setMenuWaypointFromMap called: index=$index, pos=$position'); +// if (index < 0 || index >= 2) return; +// menuWaypoints[index] = position; +// menuWaypointNames[index] = +// '${position.latitude.toStringAsFixed(4)}, ${position.longitude.toStringAsFixed(4)}'; +// isPickingWaypoint = false; +// pickingWaypointIndex = -1; +// isPickerShown = false; +// // Re-open expanded menu +// isMainBottomMenuMap = false; +// mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56); +// update(); +// } + +// void setMenuWaypointFromSearch(int index, LatLng pos, String name) { +// if (index < 0 || index >= 2) return; +// menuWaypoints[index] = pos; +// menuWaypointNames[index] = name; +// update(); +// } + +// /// Build OSRM waypoint coordinate string for the route URL +// String _buildOsrmWaypointCoords() { +// String coords = ''; +// for (int i = 0; i < activeMenuWaypointCount; i++) { +// final wp = menuWaypoints[i]; +// if (wp != null) { +// coords += ';${wp.longitude},${wp.latitude}'; +// } +// } +// return coords; +// } + +// void changeHeightPointsPageForRider() { +// isPointsPageForRider = !isPointsPageForRider; +// heightPointsPageForRider = isPointsPageForRider == true ? Get.height : 0; +// update(); +// } + +// getCoordinateFromMapWayPoints(int index) { +// placesCoordinate[index] = newStartPointLocation.toString(); +// update(); +// } +// // --- ابدأ الإضافة هنا --- + +// // 1. قائمة لتخزين نقاط التوقف +// List> waypoints = []; + +// // 2. دالة لإضافة نقطة توقف جديدة +// void addWaypoint(Map placeDetails) { +// // يمكنك إضافة منطق للتحقق من عدد نقاط التوقف المسموح بها هنا +// waypoints.add(placeDetails); +// update(); // لتحديث الواجهة +// // TODO: أضف هنا استدعاء دالة إعادة رسم المسار مع نقاط التوقف الجديدة +// // getDirectionMapWithWaypoints(); +// } + +// // 3. دالة لحذف نقطة توقف +// void removeWaypoint(int index) { +// if (index >= 0 && index < waypoints.length) { +// waypoints.removeAt(index); +// update(); // لتحديث الواجهة +// // TODO: أضف هنا استدعاء دالة إعادة رسم المسار بعد حذف النقطة +// // getDirectionMapWithWaypoints(); +// } +// } + +// // --- انتهى --- + +// void changeMainBottomMenuMap() { +// if (isWayPointStopsSheetUtilGetMap == true) { +// changeWayPointSheet(); +// } else { +// isMainBottomMenuMap = !isMainBottomMenuMap; +// mainBottomMenuMapHeight = +// isMainBottomMenuMap == true ? Get.height * .22 : Get.height * .6; +// isWayPointSheet = false; +// if (heightMenuBool == true) { +// getDrawerMenu(); +// } +// initilizeGetStorage(); +// update(); +// } +// } + +// void downPoints() { +// if (Get.find().wayPoints.length < 2) { +// isWayPointStopsSheetUtilGetMap = false; +// isWayPointSheet = false; +// wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0; +// // changeWayPointStopsSheet(); +// update(); +// } +// // changeWayPointStopsSheet(); +// // isWayPointSheet = false; +// update(); +// } + +// void changeWayPointSheet() { +// isWayPointSheet = !isWayPointSheet; +// wayPointSheetHeight = isWayPointSheet == false ? 0 : Get.height * .45; +// // if (heightMenuBool == true) { +// // getDrawerMenu(); +// // } +// update(); +// } + +// void changeWayPointStopsSheet() { +// // int waypointsLength = Get.find().wayPoints.length; + +// if (wayPointIndex > -1) { +// isWayPointStopsSheet = true; +// isWayPointStopsSheetUtilGetMap = true; +// } +// isWayPointStopsSheet = !isWayPointStopsSheet; +// wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0; +// // if (heightMenuBool == true) { +// // getDrawerMenu(); +// // } +// update(); +// } + +// changeHeightPlaces() { +// if (placesDestination.isEmpty) { +// height = 0; +// update(); +// } +// height = 150; +// update(); +// } + +// changeHeightStartPlaces() { +// if (placesStart.isEmpty) { +// height = 0; +// update(); +// } +// height = 150; +// update(); +// } + +// changeHeightPlacesAll(int index) { +// if (placeListResponseAll[index].isEmpty) { +// height = 0; +// update(); +// } +// height = 150; +// update(); +// } + +// changeHeightPlaces1() { +// if (wayPoint1.isEmpty) { +// height = 0; +// update(); +// } +// height = 150; +// update(); +// } + +// changeHeightPlaces2() { +// if (wayPoint2.isEmpty) { +// height = 0; +// update(); +// } +// height = 150; +// update(); +// } + +// changeHeightPlaces3() { +// if (wayPoint3.isEmpty) { +// height = 0; +// update(); +// } +// height = 150; +// update(); +// } + +// changeHeightPlaces4() { +// if (wayPoint4.isEmpty) { +// height = 0; +// update(); +// } +// height = 150; +// update(); +// } + +// hidePlaces() { +// height = 0; + +// update(); +// } + +// /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض + +// // double _haversineKm(double lat1, double lon1, double lat2, double lon2) { +// // const R = 6371.0; // km +// // final dLat = (lat2 - lat1) * math.pi / 180.0; +// // final dLon = (lon2 - lon1) * math.pi / 180.0; +// // final a = math.sin(dLat / 2) * math.sin(dLat / 2) + +// // math.cos(lat1 * math.pi / 180.0) * +// // math.cos(lat2 * math.pi / 180.0) * +// // math.sin(dLon / 2) * +// // math.sin(dLon / 2); +// // final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); +// // return R * c; +// // } + +// /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض +// // double _kmToLatDelta(double km) => km / 111.0; + +// // /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض) +// // double _kmToLngDelta(double km, double atLat) => +// // km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9); + +// /// حساب درجة التطابق النصي (كل كلمة تبدأ بها الاسم = 2 نقاط، يحتويها = 1 نقطة) +// // double _relevanceScore(String name, String query) { +// // final n = name.toLowerCase(); +// // final parts = +// // query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2); +// // double s = 0.0; +// // for (final p in parts) { +// // if (n.startsWith(p)) { +// // s += 2.0; +// // } else if (n.contains(p)) { +// // s += 1.0; +// // } +// // } +// // return s; +// // } +// // الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها +// // انسخ هذه الدوال والصقها داخل كلاس الكنترولر الخاص بك + +// // ----------------------------------------------------------------- +// // --== الدالة الرئيسية للبحث ==-- +// // ----------------------------------------------------------------- +// /// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها +// // انسخ هذه الدوال والصقها داخل كلاس الكنترولر الخاص بك + +// // ----------------------------------------------------------------- +// // --== الدالة الرئيسية للبحث ==-- +// // ----------------------------------------------------------------- +// /// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها +// Future getPlaces() async { +// final q = placeDestinationController.text.trim(); +// if (q.isEmpty || q.length < 3) { +// placesDestination = []; +// update(); +// return; +// } + +// final lat = passengerLocation.latitude; +// final lng = passengerLocation.longitude; +// final country = CountryPolygons.getCountryName(passengerLocation); + +// try { +// final url = +// '${AppLink.searchGeocoding}?q=${Uri.encodeComponent(q)}&lat=$lat&lng=$lng&radius=15000&country=$country'; +// final response = await CRUD().getMapSaas(link: url); + +// if (response != null && response['results'] is List) { +// List results = List.from(response['results']); +// final List filteredResults = []; +// final Set seenPlaces = {}; + +// for (final p in results) { +// final name = p['name_ar'] ?? p['name'] ?? ''; +// final district = p['district'] ?? ''; +// final plat = p['latitude']?.toString() ?? '0'; +// final plng = p['longitude']?.toString() ?? '0'; + +// final dedupeKey = +// "${name.trim().toLowerCase()}_${district.trim().toLowerCase()}"; + +// if (!seenPlaces.contains(dedupeKey)) { +// seenPlaces.add(dedupeKey); + +// p['distanceKm'] = (p['distance'] as num).toDouble() / 1000.0; +// p['latitude'] = plat; +// p['longitude'] = plng; +// p['name'] = name; +// p['address'] = p['full_address'] ?? +// (district.isNotEmpty +// ? "$district، ${p['governorate'] ?? ''}" +// : (p['governorate'] ?? '')); + +// filteredResults.add(p); +// } +// } + +// placesDestination = filteredResults; +// update(); +// } +// } catch (e) { +// Log.print('Exception in getPlaces: $e'); +// } +// } + +// // ----------------------------------------------------------------- +// // --== دوال مساعدة ==-- +// // ----------------------------------------------------------------- + +// /// تحسب المسافة بين نقطتين بالكيلومتر (معادلة هافرساين) +// double _haversineKm(double lat1, double lon1, double lat2, double lon2) { +// const R = 6371.0; // نصف قطر الأرض بالكيلومتر +// final dLat = (lat2 - lat1) * (pi / 180.0); +// final dLon = (lon2 - lon1) * (pi / 180.0); +// final rLat1 = lat1 * (pi / 180.0); +// final rLat2 = lat2 * (pi / 180.0); + +// final a = sin(dLat / 2) * sin(dLat / 2) + +// cos(rLat1) * cos(rLat2) * sin(dLon / 2) * sin(dLon / 2); +// final c = 2 * atan2(sqrt(a), sqrt(1 - a)); +// return R * c; +// } + +// /// تحسب درجة تطابق بسيطة بين اسم المكان وكلمة البحث +// double _relevanceScore(String placeName, String query) { +// if (placeName.isEmpty || query.isEmpty) return 0.0; +// final pLower = placeName.toLowerCase(); +// final qLower = query.toLowerCase(); +// if (pLower.startsWith(qLower)) return 1.0; // تطابق كامل في البداية +// if (pLower.contains(qLower)) return 0.5; // تحتوي على الكلمة +// return 0.0; +// } + +// /// تحويل كيلومتر إلى فرق درجات لخط العرض +// double _kmToLatDelta(double km) { +// const kmInDegree = 111.32; +// return km / kmInDegree; +// } + +// /// تحويل كيلومتر إلى فرق درجات لخط الطول (يعتمد على خط العرض الحالي) +// double _kmToLngDelta(double km, double latitude) { +// const kmInDegree = 111.32; +// return km / (kmInDegree * cos(latitude * (pi / 180.0))); +// } + +// // var languageCode; + +// // // تحديد اللغة حسب الإدخال +// // if (RegExp(r'[a-zA-Z]').hasMatch(placeDestinationController.text)) { +// // languageCode = 'en'; +// // } else { +// // languageCode = 'ar'; +// // } + +// // final bool isTextEmpty = placeDestinationController.text.trim().isEmpty; +// // var key = Platform.isAndroid ? AK.mapAPIKEY : AK.mapAPIKEYIOS; +// // final Uri url = Uri.parse( +// // isTextEmpty +// // ? 'https://places.googleapis.com/v1/places:searchNearby?key=$key' +// // : 'https://places.googleapis.com/v1/places:searchText?key=$key', +// // ); +// // Log.print('url: $url'); +// // // بناء الجسم حسب نوع الطلب +// // final body = isTextEmpty +// // ? jsonEncode({ +// // "languageCode": languageCode, +// // "locationRestriction": { +// // "circle": { +// // "center": { +// // "latitude": passengerLocation.latitude, +// // "longitude": passengerLocation.longitude +// // }, +// // "radius": 40000 // 40 كم +// // } +// // }, +// // "maxResultCount": 10 +// // }) +// // : jsonEncode({ +// // "textQuery": placeDestinationController.text, +// // "languageCode": languageCode, +// // "maxResultCount": 10, +// // "locationBias": { +// // "circle": { +// // "center": { +// // "latitude": passengerLocation.latitude, +// // "longitude": passengerLocation.longitude +// // }, +// // "radius": 40000 +// // } +// // } +// // }); + +// // final headers = { +// // 'Content-Type': 'application/json', +// // 'X-Goog-Api-Key': AK.mapAPIKEY, +// // 'X-Goog-FieldMask': +// // 'places.displayName,places.formattedAddress,places.location' +// // }; + +// // try { +// // final response = await http.post(url, headers: headers, body: body); +// // Log.print('response: ${response.statusCode} - ${response.body}'); + +// // if (response.statusCode == 200) { +// // final data = jsonDecode(response.body); +// // placesDestination = data['places'] ?? []; +// // update(); +// // } else { +// // Log.print('Error: ${response.statusCode} - ${response.reasonPhrase}'); +// // } +// // } catch (e) { +// // Log.print('Exception: $e'); +// // } +// // } + +// getAIKey(String key) async { +// var res = +// await CRUD().get(link: AppLink.getapiKey, payload: {"keyName": key}); +// if (res != 'failure') { +// var d = jsonDecode(res)['message']; +// return d[key].toString(); +// } else {} +// } + +// Future getPlacesStart() async { +// final q = placeStartController.text.trim(); +// if (q.isEmpty || q.length < 3) { +// placesStart = []; +// update(); +// return; +// } + +// final lat = passengerLocation.latitude; +// final lng = passengerLocation.longitude; +// final country = CountryPolygons.getCountryName(passengerLocation); + +// try { +// final url = +// '${AppLink.searchGeocoding}?q=${Uri.encodeComponent(q)}&lat=$lat&lng=$lng&radius=15000&country=$country'; +// final response = await CRUD().getMapSaas(link: url); + +// if (response != null && response['results'] is List) { +// List list = List.from(response['results']); +// for (final p in list) { +// p['distanceKm'] = (p['distance'] as num).toDouble() / 1000.0; +// p['latitude'] = p['latitude'].toString(); +// p['longitude'] = p['longitude'].toString(); +// p['name'] = p['name_ar'] ?? p['name'] ?? ''; +// p['address'] = p['full_address'] ?? +// (p['district'] != null +// ? "${p['district']}، ${p['governorate'] ?? ''}" +// : (p['governorate'] ?? '')); +// } +// placesStart = list; +// update(); +// } +// } catch (e) { +// Log.print('Exception in getPlacesStart: $e'); +// } +// } + +// Future getPlacesListsWayPoint(int index) async { +// final q = wayPoint0Controller.text.trim(); +// if (q.length < 3) return; + +// final lat = passengerLocation.latitude; +// final lng = passengerLocation.longitude; +// final country = CountryPolygons.getCountryName(passengerLocation); + +// try { +// final url = +// '${AppLink.searchGeocoding}?q=${Uri.encodeComponent(q)}&lat=$lat&lng=$lng&radius=15000&country=$country'; +// final response = await CRUD().getMapSaas(link: url); + +// if (response != null && response['results'] is List) { +// List list = List.from(response['results']); +// for (final p in list) { +// p['distanceKm'] = (p['distance'] as num).toDouble() / 1000.0; +// p['latitude'] = p['latitude'].toString(); +// p['longitude'] = p['longitude'].toString(); +// p['name'] = p['name_ar'] ?? p['name'] ?? ''; +// p['address'] = p['full_address'] ?? +// (p['district'] != null +// ? "${p['district']}، ${p['governorate'] ?? ''}" +// : (p['governorate'] ?? '')); +// } +// wayPoint0 = list; +// placeListResponseAll[index] = list; +// update(); +// } +// } catch (e) { +// Log.print('Error fetching places in WayPoint: $e'); +// } +// } + +// // داخل MapPassengerController +// bool lowPerf = false; +// Timer? _camThrottle; +// DateTime _lastUiUpdate = DateTime.fromMillisecondsSinceEpoch(0); + +// Future detectPerfMode() async { +// try { +// if (GetPlatform.isAndroid) { +// final info = await DeviceInfoPlugin().androidInfo; +// final sdk = info.version.sdkInt ?? 0; +// final ram = info.availableRamSize ?? 0; +// lowPerf = (sdk < 28) || (ram > 0 && ram < 3 * 1024 * 1024 * 1024); +// } else { +// lowPerf = false; +// } +// } catch (_) { +// lowPerf = false; +// } +// update(); +// } + +// // تحديث الكاميرا بثروتل +// void onCameraMoveThrottled(CameraPosition pos) { +// _camThrottle?.cancel(); +// _camThrottle = Timer(const Duration(milliseconds: 160), () { +// Log.print('📸 onCameraMoveThrottled: ${pos.target}'); +// // ضع فقط المنطق الضروري هنا لتقليل الحمل +// int waypointsLength = Get.find().wayPoints.length; +// int index = wayPointIndex; +// if (waypointsLength > 0) { +// placesCoordinate[index] = +// '${pos.target.latitude},${pos.target.longitude}'; +// } +// newMyLocation = pos.target; +// }); +// } + +// // Removed legacy light polylines since MapLibre vectors handle high-point geometries natively. + +// Future savePlaceToServer( +// String latitude, String longitude, String name, String rate) async { +// var data = { +// 'latitude': latitude, +// 'longitude': longitude, +// 'name': name, +// 'rate': rate, +// }; + +// try { +// CRUD().post( +// link: AppLink.savePlacesServer, +// payload: data, +// ); +// } catch (e) { +// Log.print('Error: $e'); +// } +// } + +// Future getLocation() async { +// Log.print('🛰️ getLocation() called'); +// // Check if the app has permission to access location +// permissionGranted = await location.hasPermission(); +// if (permissionGranted == PermissionStatus.denied) { +// permissionGranted = await location.requestPermission(); +// if (permissionGranted != PermissionStatus.granted) { +// // Location permission is still not granted, handle the error +// return; +// } +// } + +// // Configure location accuracy +// // LocationAccuracy desiredAccuracy = LocationAccuracy.high; + +// // Get the current location with a timeout to prevent hanging UI +// LocationData? _locationData; +// try { +// _locationData = await location.getLocation().timeout( +// const Duration(seconds: 5), +// onTimeout: () { +// Log.print("⚠️ Location fetch timed out after 5s."); +// return LocationData.fromMap({ +// "latitude": passengerLocation.latitude, +// "longitude": passengerLocation.longitude, +// "speed": 0.0 +// }); +// }, +// ); +// } catch (e) { +// Log.print("⚠️ Error fetching location: $e"); +// } + +// if (_locationData == null) { +// isLoading = false; +// update(); +// return; +// } +// passengerLocation = +// (_locationData.latitude != null && _locationData.longitude != null +// ? LatLng(_locationData.latitude!, _locationData.longitude!) +// : null)!; +// // getLocationArea(passengerLocation.latitude, passengerLocation.longitude); +// // Log.print('AppLink.endPoint: ${AppLink.endPoint}'); +// // Log.print('BoxName.serverChosen: ${box.read(BoxName.serverChosen)}'); + +// newStartPointLocation = passengerLocation; +// newMyLocation = passengerLocation; + +// // Resolve current location address +// try { +// getReverseGeocoding(passengerLocation).then((address) { +// currentLocationString = address; +// update(); +// }); +// } catch (e) { +// Log.print('Error resolving current location: $e'); +// } + +// // Trigger offline map caching for a 10km radius +// OfflineMapService.instance +// .downloadRegion(passengerLocation, radiusKm: 10.0); + +// speed = _locationData.speed!; +// // //print location details +// isLoading = false; +// update(); +// } + +// void clearPolyline() { +// polyLines.clear(); +// update(); +// } + +// LatLngBounds calculateBounds(double lat, double lng, double radiusInMeters) { +// const double earthRadius = 6378137.0; // Earth's radius in meters + +// double latDelta = (radiusInMeters / earthRadius) * (180 / pi); +// double lngDelta = +// (radiusInMeters / (earthRadius * cos(pi * lat / 180))) * (180 / pi); + +// double minLat = lat - latDelta; +// double maxLat = lat + latDelta; + +// double minLng = lng - lngDelta; +// double maxLng = lng + lngDelta; + +// // Ensure the latitude is between -90 and 90 +// minLat = max(-90.0, minLat); +// maxLat = min(90.0, maxLat); + +// // Ensure the longitude is between -180 and 180 +// minLng = (minLng + 180) % 360 - 180; +// maxLng = (maxLng + 180) % 360 - 180; + +// // Ensure the bounds are in the correct order +// if (minLng > maxLng) { +// double temp = minLng; +// minLng = maxLng; +// maxLng = temp; +// } + +// return LatLngBounds( +// southwest: LatLng(minLat, minLng), +// northeast: LatLng(maxLat, maxLng), +// ); +// } + +// void onMapCreated(IntaleqMapController controller) { +// mapController = controller; +// update(); +// } + +// void onStyleLoaded() async { +// Log.print('🗺️ Intaleq Map Style Loaded. Initializing...'); +// isStyleLoaded = true; +// _loadMapIcons(); + +// // Smart Camera Reset logic: +// if (mapController != null) { +// if (markers.isNotEmpty && lastComputedBounds != null) { +// await _safeAnimateCameraBounds(lastComputedBounds); +// } else { +// mapController!.animateCamera( +// CameraUpdate.newLatLng(passengerLocation), +// ); +// } +// } +// update(); +// } + +// /// Safe wrapper for animateCamera Bounds to prevent native std::domain_error crash on iOS. +// Future _safeAnimateCameraBounds(LatLngBounds? bounds, +// {double left = 60, +// double top = 60, +// double right = 60, +// double bottom = 60}) async { +// if (bounds == null || mapController == null) return; + +// try { +// // Ensure the coordinates are valid +// if (bounds.northeast.latitude == bounds.southwest.latitude && +// bounds.northeast.longitude == bounds.southwest.longitude) { +// Log.print( +// '⚠️ _safeAnimateCameraBounds: Bounds are a single point, zooming to point instead.'); +// await mapController +// ?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 15)); +// return; +// } + +// // Small delay to ensure iOS view layout is fully ready +// await Future.delayed(const Duration(milliseconds: 200)); + +// await mapController?.animateCamera( +// CameraUpdate.newLatLngBounds( +// bounds, +// left: left, +// top: top, +// right: right, +// bottom: bottom, +// ), +// ); +// } catch (e) { +// Log.print('❌ _safeAnimateCameraBounds CRASH PREVENTED: $e'); +// // Final fallback to prevent device freeze +// try { +// await mapController +// ?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 14)); +// } catch (_) {} +// } +// } + +// Future _loadMapIcons() async { +// // Wait up to 3 seconds for the map style to finish loading +// for (int i = 0; i < 15; i++) { +// if (mapController != null && isStyleLoaded) break; +// await Future.delayed(const Duration(milliseconds: 200)); +// } + +// if (mapController == null || !isStyleLoaded) { +// Log.print( +// '⚠️ _loadMapIcons: mapController or style not ready. Icons may not load.'); +// } + +// await _addMapImage(startIcon, 'assets/images/A.png'); +// await _addMapImage(endIcon, 'assets/images/b.png'); +// await _addMapImage(carIcon, 'assets/images/car.png'); +// await _addMapImage(motoIcon, 'assets/images/moto.png'); +// await _addMapImage(ladyIcon, 'assets/images/lady.png'); +// await _addMapImage('picker_icon', 'assets/images/picker.png'); +// // Waypoint markers - use moto1 & lady1 as colored waypoint icons +// await _addMapImage('orange_marker', 'assets/images/moto1.png'); +// await _addMapImage('violet_marker', 'assets/images/lady1.png'); +// } + +// Future _addMapImage(String id, String path) async { +// try { +// final ByteData bytes = await rootBundle.load(path); +// // Resize car icons for better visibility on map (e.g. 120px) +// final size = _getImageSize(id); +// if (size != null && (id == carIcon || id == motoIcon || id == ladyIcon)) { +// final resized = await _resizeImage(bytes.buffer.asUint8List(), size); +// await mapController?.addImage(id, resized); +// Log.print( +// '✅ Successfully added resized map image: $id (${size}x${size})'); +// } else { +// await mapController?.addImage(id, bytes.buffer.asUint8List()); +// Log.print('✅ Successfully added map image: $id'); +// } +// } catch (e) { +// Log.print('❌ Error loading map icon $id: $e'); +// } +// } + +// int? _getImageSize(String id) { +// if (id == carIcon || id == motoIcon || id == ladyIcon) return 120; +// return null; +// } + +// Future _resizeImage(Uint8List bytes, int size) async { +// return await compute((Uint8List data) { +// final image = img.decodeImage(data); +// if (image == null) return data; +// final resized = img.copyResize(image, width: size, height: size); +// return Uint8List.fromList(img.encodePng(resized)); +// }, bytes); +// } + +// // Wait up to 3 seconds for the map style to finish loading + +// void updateCurrentLocationFromCamera(LatLng target) { +// Log.print('📍 updateCurrentLocationFromCamera: $target'); +// newMyLocation = target; + +// if (startLocationFromMap == true) { +// Log.print('📍 Updating startLocationFromMap to $target'); +// newStartPointLocation = target; +// } else if (passengerStartLocationFromMap == true) { +// Log.print('📍 Updating passengerStartLocationFromMap to $target'); +// newStartPointLocation = target; +// } + +// int waypointsLength = Get.find().wayPoints.length; +// if (waypointsLength > 0 && +// wayPointIndex >= 0 && +// wayPointIndex < placesCoordinate.length) { +// Log.print('📍 Updating wayPointIndex $wayPointIndex to $target'); +// placesCoordinate[wayPointIndex] = +// '${target.latitude},${target.longitude}'; +// } +// update(); +// } + +// String durationByPassenger = ''; +// late DateTime newTime1 = DateTime.now(); +// late DateTime timeFromDriverToPassenger = DateTime.now(); +// String distanceByPassenger = ''; +// late Duration durationFromDriverToPassenger; +// double nearestDistance = double.infinity; + +// Future getNearestDriverByPassengerLocation() async { +// if (!rideConfirm) { +// if (dataCarsLocationByPassenger != 'failure' && +// dataCarsLocationByPassenger != null && +// dataCarsLocationByPassenger['message'] != null && +// dataCarsLocationByPassenger['message'].length > 0) { +// double nearestDistance = double.infinity; // Initialize nearest distance +// CarLocation? nearestCar; + +// for (var i = 0; +// i < dataCarsLocationByPassenger['message'].length; +// i++) { +// var carLocation = dataCarsLocationByPassenger['message'][i]; +// // Log.print('carLocation: $carLocation'); + +// try { +// // Calculate distance between passenger's location and current driver's location +// final distance = Geolocator.distanceBetween( +// passengerLocation.latitude, +// passengerLocation.longitude, +// double.parse(carLocation['latitude']), +// double.parse(carLocation['longitude']), +// ); + +// // Calculate duration assuming an average speed of 25 km/h (adjust as needed) +// int durationToPassenger = (distance / 1000 / 25 * 3600).round(); +// // Log.print('distance: $distance'); +// // Log.print('durationToPassenger: $durationToPassenger'); +// // Log.print('passengerLocation: $passengerLocation'); +// // Log.print('carLocation: $carLocation'); +// // Log.print('distance: $distance meters'); +// // Log.print('durationToPassenger: $durationToPassenger seconds'); +// // Update the UI with the distance and duration for each car +// update(); + +// // If this distance is smaller than the nearest distance found so far, update nearestCar +// if (distance < nearestDistance) { +// nearestDistance = distance; + +// nearestCar = CarLocation( +// distance: distance, +// duration: durationToPassenger.toDouble(), +// id: carLocation['driver_id'], +// latitude: double.parse(carLocation['latitude']), +// longitude: double.parse(carLocation['longitude']), +// ); +// // Log.print('nearestCar: $nearestCar'); +// // Update the UI with the nearest driver +// update(); +// } +// } catch (e) { +// Log.print('Error calculating distance/duration: $e'); +// } +// } + +// // Return the nearest car found +// return nearestCar; +// } +// } + +// // Return null if no drivers are found or if ride is confirmed +// return null; +// } + +// getNearestDriverByPassengerLocationAPIGOOGLE() async { +// if (polyLines.isEmpty || data.isEmpty) { +// return null; // Early return if data is empty +// } +// if (!rideConfirm) { +// double nearestDistance = double.infinity; +// if (dataCarsLocationByPassenger != 'failure') { +// if (dataCarsLocationByPassenger['message'].length > 0) { +// for (var i = 0; +// i < dataCarsLocationByPassenger['message'].length; +// i++) { +// var carLocation = dataCarsLocationByPassenger['message'][i]; + +// // } +// // isloading = true; +// update(); +// // Make API request to get exact distance and duration +// String apiUrl = +// '${AppLink.googleMapsLink}distancematrix/json?destinations=${carLocation['latitude']},${carLocation['longitude']}&origins=${passengerLocation.latitude},${passengerLocation.longitude}&units=metric&key=${AK.mapAPIKEY}'; +// var response = await CRUD().getGoogleApi(link: apiUrl, payload: {}); +// if (response != null && response['status'] == "OK") { +// var data = response; +// // Extract distance and duration from the response and handle accordingly +// int distance1 = +// data['rows'][0]['elements'][0]['distance']['value']; +// distanceByPassenger = +// data['rows'][0]['elements'][0]['distance']['text']; +// durationToPassenger = +// data['rows'][0]['elements'][0]['duration']['value']; + +// durationFromDriverToPassenger = +// Duration(seconds: durationToPassenger.toInt()); +// newTime1 = currentTime.add(durationFromDriverToPassenger); +// timeFromDriverToPassenger = +// newTime1.add(Duration(minutes: 2.toInt())); +// durationByPassenger = +// data['rows'][0]['elements'][0]['duration']['text']; +// update(); +// if (distance1 < nearestDistance) { +// nearestDistance = distance1.toDouble(); + +// nearestCar = CarLocation( +// distance: distance1.toDouble(), +// duration: durationToPassenger.toDouble(), +// id: carLocation['driver_id'], +// latitude: double.parse(carLocation['latitude']), +// longitude: double.parse(carLocation['longitude']), +// ); +// // isloading = false; +// update(); +// } +// } + +// // Handle the distance and duration as needed +// else { +// // 'Failed to retrieve distance and duration: ${response['status']}'); +// Log.print('${response['status']}: ${response['status']}}'); +// // Handle the failure case +// } +// } +// } +// } +// } +// } + +// calculateDistanceBetweenPassengerAndDriverBeforeCancelRide() async { +// await getDriverCarsLocationToPassengerAfterApplied(); +// double distance = Geolocator.distanceBetween( +// passengerLocation.latitude, +// passengerLocation.longitude, +// driverCarsLocationToPassengerAfterApplied.last.latitude, +// driverCarsLocationToPassengerAfterApplied.last.longitude, +// ); +// if (distance > 500) { +// isCancelRidePageShown = true; +// update(); +// } else { +// Get.defaultDialog( +// barrierDismissible: false, +// title: 'The Driver Will be in your location soon .'.tr, +// middleText: 'The distance less than 500 meter.'.tr, +// confirm: Column( +// children: [ +// MyElevatedButton( +// kolor: AppColor.greenColor, +// title: 'Ok'.tr, +// onPressed: () { +// Get.back(); +// }, +// ), +// MyElevatedButton( +// kolor: AppColor.redColor, +// title: 'No, I want to cancel this trip'.tr, +// onPressed: () { +// Get.back(); +// MyDialog().getDialog( +// 'Attention'.tr, +// 'You will be charged for the cost of the driver coming to your location.' +// .tr, +// () async { +// Get.back(); +// Get.find() +// .payToDriverForCancelAfterAppliedAndHeNearYou(rideId); +// // isCancelRidePageShown = true; +// // update(); +// }, +// ); +// }, +// ), +// ], +// ), +// ); +// // cancel: MyElevatedButton( +// // title: 'No.Iwant Cancel Trip.'.tr, onPressed: () {})); +// } +// } + +// List headingAngles = []; +// double calculateAngleBetweenLocations(LatLng start, LatLng end) { +// double startLat = start.latitude * math.pi / 180; +// double startLon = start.longitude * math.pi / 180; +// double endLat = end.latitude * math.pi / 180; +// double endLon = end.longitude * math.pi / 180; + +// double dLon = endLon - startLon; + +// double y = math.sin(dLon) * cos(endLat); +// double x = cos(startLat) * math.sin(endLat) - +// math.sin(startLat) * cos(endLat) * cos(dLon); + +// double angle = math.atan2(y, x); +// double angleDegrees = angle * 180 / math.pi; + +// return angleDegrees; +// } + +// late LatLngBounds boundsData; +// late String startNameAddress = ''; +// late String endNameAddress = ''; +// List> stopPoints = []; +// void removeStop(Map stop) { +// stopPoints.remove(stop); +// update(); // Trigger a rebuild of the UI +// } + +// Future getReverseGeocoding(LatLng location) async { +// final lat = location.latitude; +// final lng = location.longitude; +// final url = '${AppLink.reverseGeocoding}?lat=$lat&lng=$lng'; + +// try { +// final response = await CRUD().getMapSaas(link: url); + +// if (response != null && response is List && response.isNotEmpty) { +// final data = response[0]; +// String name = data['name_ar'] ?? data['name'] ?? 'Unknown Location'.tr; +// return name; +// } +// return 'Unknown Location'.tr; +// } catch (e) { +// Log.print('ReverseGeocoding Exception: $e'); +// return 'Unknown Location'.tr; +// } +// } + +// bool isDrawingRoute = false; +// void showDrawingBottomSheet() { +// Log.print( +// '🔔 showDrawingBottomSheet called. isDrawingRoute: $isDrawingRoute'); + +// final context = Get.context; +// if (context == null) return; + +// WidgetsBinding.instance.addPostFrameCallback((_) { +// // Close any existing open dialogs first +// if (Get.isDialogOpen == true) { +// Get.back(); +// } + +// Get.dialog( +// Dialog( +// backgroundColor: Colors.transparent, +// elevation: 0, +// child: Container( +// padding: const EdgeInsets.all(24), +// width: 180, +// decoration: BoxDecoration( +// color: Colors.white.withOpacity(0.95), +// borderRadius: BorderRadius.circular(24), +// boxShadow: [ +// BoxShadow( +// color: Colors.black.withOpacity(0.15), +// blurRadius: 20, +// spreadRadius: 5, +// ) +// ], +// ), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// mainAxisAlignment: MainAxisAlignment.center, +// crossAxisAlignment: CrossAxisAlignment.center, +// children: [ +// // App Logo +// Image.asset( +// 'assets/images/logo.gif', +// height: 64, +// errorBuilder: (context, error, stackTrace) => const Icon( +// Icons.map, +// size: 64, +// color: AppColor.primaryColor, +// ), +// ), +// const SizedBox(height: 16), +// const SizedBox( +// width: 24, +// height: 24, +// child: MyCircularProgressIndicator(), +// ), +// const SizedBox(height: 16), +// Text( +// 'Drawing route on map...'.tr, +// style: const TextStyle( +// fontWeight: FontWeight.bold, +// fontSize: 14, +// color: AppColor.primaryColor, +// ), +// textAlign: TextAlign.center, +// ), +// ], +// ), +// ), +// ), +// barrierDismissible: false, +// ); + +// // Auto-dismiss after exactly 2 seconds +// Future.delayed(const Duration(seconds: 2), () { +// if (Get.isDialogOpen == true) { +// Get.back(); +// } +// }); +// }); +// } + +// String dynamicApiUrl = 'https://routec.intaleq.xyz/route'; +// Future getDistanceFromDriverAfterAcceptedRide( +// String origin, String destination) async { +// String apiKey = Env.mapKeyOsm; // مفتاح API الخاص بك +// if (origin.isEmpty) { +// origin = '${passengerLocation.latitude},${passengerLocation.longitude}'; +// } +// // 2. بناء الرابط (URI) +// // Waypoints غير مدعومة حالياً في OSRM، لذلك تم تجاهلها +// var uri = Uri.parse( +// '$dynamicApiUrl?origin=$origin&destination=$destination&steps=false&overview=false'); +// Log.print('uri: $uri'); + +// // 3. إرسال الطلب مع الهيدر +// http.Response response; +// Map responseData; + +// try { +// response = await http.get( +// uri, +// headers: { +// 'X-API-KEY': apiKey, +// }, +// ).timeout(const Duration(seconds: 20)); // تايم آوت 20 ثانية + +// if (response.statusCode != 200) { +// Log.print('Error from API: ${response.statusCode}'); +// isLoading = false; +// update(); +// return; // خروج في حالة الخطأ +// } +// if (Get.isBottomSheetOpen ?? false) { +// Get.back(); // لإغلاق شاشة "جاري الرسم" +// } +// isDrawingRoute = false; // Reset state + +// responseData = json.decode(response.body); +// Log.print('responseData: $responseData'); + +// if (responseData['status'] != 'ok') { +// Log.print('API returned an error: ${responseData['message']}'); +// isLoading = false; +// update(); +// return; // خروج في حالة خطأ منطقي (مثل "no path") +// } +// } catch (e) { +// Log.print('Failed to get directions: $e'); +// isLoading = false; +// update(); +// return; // خروج عند فشل الاتصال +// } +// } + +// // (b = 1.5348) هو المعامل الذي تم حسابه من مقارنة 60 رحلة بين Google و OSRM +// double kDurationScalar = +// 1.5348; //this from colab 60 random locations from google and routec + +// // ----------------------------------------------------------------------------------------- +// // GET DIRECTION MAP (FULL) +// // ----------------------------------------------------------------------------------------- +// // ----------------------------------------------------------------------------------------- +// // GET DIRECTION MAP (With Auto-Retry Logic) +// // ----------------------------------------------------------------------------------------- +// // أضفنا attemptCount لتتبع عدد المحاولات +// // ----------------------------------------------------------------------------------------- +// // GET DIRECTION MAP (Retry or Fail Strict Logic) +// // ----------------------------------------------------------------------------------------- +// Future getDirectionMap(String origin, String destination, +// [List waypoints = const [], int attemptCount = 0]) async { +// // 1. إظهار التحميل فقط في المحاولة الأولى +// if (attemptCount == 0) { +// // NOTE: Do NOT set isLoading = true here! +// // isLoading destroys the MapLibreMap widget entirely (replaced by spinner), +// // which means markers/polylines cannot be added to the new map instance +// // until its style finishes loading asynchronously — causing a race condition. +// // The showDrawingBottomSheet() overlay provides sufficient user feedback. +// isDrawingRoute = true; +// update(); +// if (isDrawingRoute) showDrawingBottomSheet(); + +// await getCarsLocationByPassengerAndReloadMarker(); +// } + +// // تجهيز الإحداثيات +// if (origin.isEmpty) { +// origin = '${passengerLocation.latitude},${passengerLocation.longitude}'; +// } + +// var coordDestination = destination.split(','); +// double latDest = double.parse(coordDestination[0]); +// double lngDest = double.parse(coordDestination[1]); +// myDestination = LatLng(latDest, lngDest); + +// // ── 2. Unified SaaS Routing Strategy ────────────────────────── +// final bool isSaaSRequest = true; +// Uri uri; + +// var originCoords = origin.split(','); +// final Map queryParams = { +// 'fromLat': originCoords[0].trim(), +// 'fromLng': originCoords[1].trim(), +// 'toLat': latDest.toString(), +// 'toLng': lngDest.toString(), +// }; + +// // Add multi-stop waypoints to the query parameters +// for (int i = 0; i < activeMenuWaypointCount; i++) { +// final wp = menuWaypoints[i]; +// if (wp != null) { +// queryParams['stop${i + 1}Lat'] = wp.latitude.toString(); +// queryParams['stop${i + 1}Lng'] = wp.longitude.toString(); +// } +// } + +// uri = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams); + +// Log.print( +// 'Requesting Route URI (${isSaaSRequest ? "SaaS" : "OSRM"}, Attempt: ${attemptCount + 1}): $uri'); + +// http.Response response; +// Map responseData; + +// try { +// response = await http.get(uri, headers: { +// 'x-api-key': Env.mapSaasKey, +// }).timeout(const Duration(seconds: 20)); + +// responseData = json.decode(response.body); + +// // Validation: SaaS returns 200 with data, OSRM returns code: 'Ok' +// bool isRequestValid = response.statusCode == 200 && +// (isSaaSRequest || responseData['code'] == 'Ok'); + +// if (!isRequestValid) { +// if (attemptCount < 2) { +// await _retryProcess(origin, destination, waypoints, attemptCount); +// return; +// } +// _handleFatalError( +// "Server Error".tr, "Connection failed. Please try again.".tr); +// return; +// } + +// // ============================================================ +// // 🛑 الفحص الأمني (Sanity Check) +// // ============================================================ +// double apiDistanceMeters; +// String pointsString; +// dynamic routeData; + +// // SaaS parsing +// apiDistanceMeters = (responseData['distance'] as num).toDouble(); +// pointsString = responseData['points'] ?? ""; +// routeData = responseData; // For box storage + +// var originCoords = origin.split(','); +// double startLat = double.parse(originCoords[0]); +// double startLng = double.parse(originCoords[1]); + +// // المسافة الجوية +// double aerialDistance = +// Geolocator.distanceBetween(startLat, startLng, latDest, lngDest); + +// if (apiDistanceMeters < 50.0 && aerialDistance > 200.0) { +// Log.print( +// "⚠️ Suspicious Route detected! Server: $apiDistanceMeters m | Aerial: $aerialDistance m"); + +// if (attemptCount < 2) { +// Log.print("🔄 Retrying request (Attempt ${attemptCount + 2})..."); +// await Future.delayed(const Duration(seconds: 1)); +// await getDirectionMap( +// origin, destination, waypoints, attemptCount + 1); +// return; +// } else { +// Log.print("❌ All retries failed. Calculating Route is impossible."); +// _handleFatalError( +// "Route Not Found".tr, +// "We couldn't find a valid route to this destination. Please try selecting a different point." +// .tr); +// return; +// } +// } + +// // 3. معالجة البيانات +// box.remove(BoxName.tripData); +// box.write(BoxName.tripData, routeData); + +// durationToRide = +// ((routeData['duration'] as num) * kDurationScalar).toInt(); +// double distanceOfTrip = apiDistanceMeters / 1000.0; +// distance = distanceOfTrip; + +// data = routeData['legs'] != null && routeData['legs'].isNotEmpty +// ? (routeData['legs'][0]['steps'] ?? []) +// : []; + +// List decodedPoints = []; +// if (pointsString.isNotEmpty) { +// decodedPoints = await compute(decodePolylineIsolate, pointsString); +// } + +// if (decodedPoints.isEmpty) { +// _handleFatalError("Map Error".tr, "Received empty route data.".tr); +// return; +// } + +// polylineCoordinates.clear(); +// polylineCoordinates.addAll(decodedPoints); + +// final LatLng startLoc = polylineCoordinates.first; +// final LatLng endLoc = polylineCoordinates.last; + +// // ── 4. العناوين والتحديثات ────────────────────────────────── +// startNameAddress = responseData['startName'] ?? 'Start Point'.tr; +// endNameAddress = responseData['endName'] ?? 'Destination'.tr; +// Log.print('📍 ROUTE START: $startNameAddress'); +// Log.print('📍 ROUTE END: $endNameAddress'); + +// // ── 5. Bounds Calculation (SaaS bbox vs OSRM manual) ────────── +// if (isSaaSRequest && responseData['bbox'] != null) { +// List bbox = responseData['bbox']; +// if (bbox.length == 4) { +// // SaaS format: [minLng, minLat, maxLng, maxLat] +// lastComputedBounds = LatLngBounds( +// southwest: LatLng(bbox[1], bbox[0]), +// northeast: LatLng(bbox[3], bbox[2]), +// ); +// } +// } else { +// double? minLat, maxLat, minLng, maxLng; +// for (LatLng point in polylineCoordinates) { +// minLat = +// minLat == null ? point.latitude : min(minLat, point.latitude); +// maxLat = +// maxLat == null ? point.latitude : max(maxLat, point.latitude); +// minLng = +// minLng == null ? point.longitude : min(minLng, point.longitude); +// maxLng = +// maxLng == null ? point.longitude : max(maxLng, point.longitude); +// } +// if (minLat != null) { +// lastComputedBounds = LatLngBounds( +// northeast: LatLng(maxLat!, maxLng!), +// southwest: LatLng(minLat!, minLng!)); +// } +// } +// // isDrawingRoute = false; +// // 5b. Reset state when finished +// if (isDrawingRoute) { +// Log.print('🔔 Finalizing route drawing state'); +// isDrawingRoute = false; +// isLoading = false; +// update(); +// } + +// // 6. إضافة الماركرز +// durationToAdd = Duration(seconds: durationToRide); +// hours = durationToAdd.inHours; +// minutes = (durationToAdd.inMinutes % 60).round(); + +// markers = { +// Marker( +// markerId: const MarkerId('start'), +// position: startLoc, +// icon: InlqBitmap.fromStyleImage('orange_marker'), +// infoWindow: const InfoWindow(title: 'A'), +// anchor: const Offset(0.5, 1.0), +// ), +// Marker( +// markerId: const MarkerId('end'), +// position: endLoc, +// icon: InlqBitmap.fromStyleImage('violet_marker'), +// infoWindow: const InfoWindow(title: 'B'), +// anchor: const Offset(0.5, 1.0), +// ), +// }; + +// for (int i = 0; i < activeMenuWaypointCount; i++) { +// final wp = menuWaypoints[i]; +// if (wp != null) { +// final bool isFirstWaypoint = i == 0; +// markers.add(Marker( +// markerId: MarkerId('waypoint_$i'), +// position: wp, +// icon: InlqBitmap.fromStyleImage( +// isFirstWaypoint ? 'orange_marker' : 'violet_marker'), +// infoWindow: +// InfoWindow(title: isFirstWaypoint ? 'Stop 1' : 'Stop 2'), +// anchor: const Offset(0.5, 1.0), +// )); +// } +// } + +// // 7. رسم الخط +// if (polyLines.isNotEmpty) clearPolyline(); + +// rideConfirm = false; +// isMarkersShown = true; +// update(); // تحديث أولي لإظهار الخريطة والماركرز + +// // إظهار الباتم شيت للسعر +// await bottomSheet(); + +// // تشغيل الأنيميشن الخفيف لومضات المسار + fit camera after +// await _playRouteAnimation(polylineCoordinates, lastComputedBounds); +// } catch (e, stackTrace) { +// // 🚨 Cleanup on error to prevent UI freeze +// if (isDrawingRoute) { +// isDrawingRoute = false; +// isLoading = false; +// update(); +// } + +// Log.print('🚨 CRITICAL ERROR IN getDirectionMap: $e'); +// Log.print('🚨 STACKTRACE: $stackTrace'); + +// if (attemptCount < 2) { +// await _retryProcess(origin, destination, waypoints, attemptCount); +// } else { +// _handleFatalError("Connection Error".tr, +// "Please check your internet and try again.".tr); +// } +// } +// } + +// // --- رسم المسار النهائي مع تقسيم ملون حسب نقاط التوقف --- +// Future _playRouteAnimation( +// List coords, LatLngBounds? bounds) async { +// // Segment colors matching UI dots: green → amber → purple → red +// const List segmentColors = [ +// Color(0xFF109642), // Green (start → stop 1) +// Color(0xFFF59E0B), // Amber (stop 1 → stop 2) +// Color(0xFF7C3AED), // Purple (last segment → dest) +// Color(0xFFEF4444), // Red (fallback) +// ]; + +// // ── Build final polyline segments ─────────────────────────────────── +// // Build all segments in a temporary Set first, then assign once +// Set newPolylines = {}; + +// if (activeMenuWaypointCount > 0) { +// List splitIndices = []; +// for (int w = 0; w < activeMenuWaypointCount; w++) { +// final wp = menuWaypoints[w]; +// if (wp == null) continue; +// int bestIdx = 0; +// double bestDist = double.infinity; +// for (int j = 0; j < coords.length; j++) { +// final dx = coords[j].latitude - wp.latitude; +// final dy = coords[j].longitude - wp.longitude; +// final d = dx * dx + dy * dy; +// if (d < bestDist) { +// bestDist = d; +// bestIdx = j; +// } +// } +// splitIndices.add(bestIdx); +// } +// splitIndices.sort(); + +// List boundaries = [0, ...splitIndices, coords.length - 1]; +// for (int s = 0; s < boundaries.length - 1; s++) { +// int from = boundaries[s]; +// int to = boundaries[s + 1] + 1; +// if (to > coords.length) to = coords.length; +// if (from >= to - 1) continue; +// final segCoords = coords.sublist(from, to); +// if (segCoords.length < 2) continue; +// final color = segmentColors[s % segmentColors.length]; + +// newPolylines.add(Polyline( +// polylineId: PolylineId('segment_$s'), +// points: segCoords, +// color: color, +// width: 6, +// )); +// } +// } else { +// newPolylines.add(Polyline( +// polylineId: const PolylineId('route_primary'), +// points: coords, +// color: AppColor.primaryColor, +// width: 6, +// )); +// } + +// polyLines = newPolylines; +// update(); + +// Log.print( +// '🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map'); + +// update(); + +// // ── Fit camera to full route bounds ──────────────────────────────── +// if (bounds != null) { +// await _safeAnimateCameraBounds(bounds); +// } +// } + +// // --- دالة المساعدة لإعادة المحاولة --- +// Future _retryProcess(String origin, String dest, List waypoints, +// int currentAttempt) async { +// Log.print( +// "🔄 Exception or Error caught. Retrying in 1s... (Attempt ${currentAttempt + 1})"); +// await Future.delayed(const Duration(seconds: 1)); +// getDirectionMap(origin, dest, waypoints, currentAttempt + 1); +// } + +// // --- دالة جديدة لتنظيف الخريطة بالكامل ومنع تداخل الرحلات --- +// void resetAllMapStates() { +// Log.print('🧹 Resetting all map states to prevent sticky location bug'); + +// clearPlacesDestination(); +// clearPlacesStart(); +// clearPolyline(); +// data = []; + +// passengerStartLocationFromMap = false; +// startLocationFromMap = false; +// isPickerShown = false; +// workLocationFromMap = false; +// homeLocationFromMap = false; +// isAnotherOreder = false; +// isWhatsAppOrder = false; + +// // ✅ أضف هذا: reset الوجهة لموقع الراكب حتى لا تبقى قيمة الرحلة القديمة +// myDestination = passengerLocation; +// hintTextDestinationPoint = 'Select your destination'.tr; + +// placeDestinationController.clear(); +// placeStartController.clear(); + +// rideConfirm = false; +// shouldFetch = false; +// isDrawingRoute = false; +// isLoading = false; + +// update(); +// } + +// // ----------------------------------------------------------------------------------------- +// // 🛑 دالة الخطأ القاتل (تغلق كل شيء وتعيد المستخدم للخريطة) +// // ----------------------------------------------------------------------------------------- +// void _handleFatalError(String title, String message) { +// // 1. إغلاق شاشة التحميل (Drawing route...) +// if (Get.isBottomSheetOpen == true || Get.isDialogOpen == true) { +// Get.back(); +// } +// if (Get.isSnackbarOpen) Get.closeCurrentSnackbar(); + +// // 2. تصفير المتغيرات +// isDrawingRoute = false; +// isLoading = false; +// update(); + +// // 3. إظهار الديالوج الإجباري +// Get.defaultDialog( +// title: title, +// titleStyle: AppStyle.title.copyWith(color: AppColor.redColor), +// middleText: message, +// middleTextStyle: AppStyle.subtitle, +// barrierDismissible: false, // لا يمكن إغلاقه بالضغط خارجاً +// confirm: MyElevatedButton( +// title: "Close".tr, +// kolor: AppColor.redColor, +// onPressed: () { +// Get.back(); // إغلاق الديالوج + +// // 4. إعادة تحميل الصفحة بالكامل (تنظيف الحالة) +// // تأكد من استيراد MapPagePassenger +// Get.offAll(() => const MapPagePassenger()); +// }, +// ), +// ); +// } + +// // Legacy gradient and layered animations removed for MapLibre migration + +// String shortenAddress(String fullAddress) { +// // Split the address into parts +// List parts = fullAddress.split('،'); + +// // Remove any leading or trailing whitespace from each part +// parts = parts.map((part) => part.trim()).toList(); + +// // Remove any empty parts +// parts = parts.where((part) => part.isNotEmpty).toList(); + +// // Initialize the short address +// String shortAddress = ''; + +// if (parts.isNotEmpty) { +// // Add the first part (usually the most specific location) +// shortAddress += parts[0]; +// } + +// if (parts.length > 2) { +// // Add the district or area name (usually the third part in Arabic format) +// shortAddress += '، ${parts[2]}'; +// } else if (parts.length > 1) { +// // Add the second part for English or shorter addresses +// shortAddress += '، ${parts[1]}'; +// } + +// // Add the country (usually the last part) +// if (parts.length > 1) { +// shortAddress += '، ${parts.last}'; +// } + +// // Remove any part that's just numbers (like postal codes) +// shortAddress = shortAddress +// .split('،') +// .where((part) => !RegExp(r'^[0-9 ]+$').hasMatch(part.trim())) +// .join('،'); + +// // Check if the address is in English +// bool isEnglish = +// RegExp(r'^[a-zA-Z0-9 ]+$').hasMatch(shortAddress.replaceAll('،', '')); + +// if (isEnglish) { +// // Further processing for English addresses +// List englishParts = shortAddress.split('،'); +// if (englishParts.length > 2) { +// shortAddress = +// '${englishParts[0]}، ${englishParts[1]}، ${englishParts.last}'; +// } else if (englishParts.length > 1) { +// shortAddress = '${englishParts[0]}، ${englishParts.last}'; +// } +// } + +// return shortAddress; +// } + +// double distanceOfDestination = 0; +// bool haveSteps = false; +// late LatLng latestPosition; + +// getMapPoints(String originSteps, String destinationSteps, int index) async { +// isWayPointStopsSheetUtilGetMap = false; +// // haveSteps = true; +// // startCarLocationSearch(box.read(BoxName.carType)); +// await getCarsLocationByPassengerAndReloadMarker(); +// // await getCarsLocationByPassengerAndReloadMarker(); +// // isLoading = true; +// update(); +// var url = +// ('${AppLink.googleMapsLink}directions/json?&language=${box.read(BoxName.lang)}&avoid=tolls|ferries&destination=$destinationSteps&origin=$originSteps&key=${AK.mapAPIKEY}'); +// var response = await CRUD().getGoogleApi(link: url, payload: {}); + +// data = response['routes'][0]['legs']; +// // isLoading = false; + +// int durationToRide0 = data[0]['duration']['value']; +// durationToRide = durationToRide + durationToRide0; +// distance = distanceOfDestination + (data[0]['distance']['value']) / 1000; + +// update(); +// // final points = +// // decodePolyline(response["routes"][0]["overview_polyline"]["points"]); +// final String pointsString = +// response['routes'][0]["overview_polyline"]["points"]; + +// List decodedPoints = +// await compute(decodePolylineIsolate, pointsString); +// // decodePolyline(response["routes"][0]["overview_polyline"]["points"]); +// for (int i = 0; i < decodedPoints.length; i++) { +// polylineCoordinates.add(decodedPoints[i]); +// } +// // Define the northeast and southwest coordinates + +// if (polyLines.isEmpty) { +// var polyline = Polyline( +// polylineId: PolylineId('route_$index'), +// points: polylineCoordinatesPointsAll[index], +// width: 6, +// color: const Color(0xFF2196F3), +// ); + +// polyLines = {...polyLines, polyline}; +// rideConfirm = false; +// update(); +// } +// } + +// void updateCameraForDistanceAfterGetMap() { +// LatLng coord1 = LatLng( +// double.parse(coordinatesWithoutEmpty.first.split(',')[0]), +// double.parse(coordinatesWithoutEmpty.first.split(',')[1])); + +// LatLng coord2 = LatLng( +// double.parse(coordinatesWithoutEmpty.last.split(',')[0]), +// double.parse(coordinatesWithoutEmpty.last.split(',')[1])); + +// LatLng northeast; +// LatLng southwest; + +// if (coord1.latitude > coord2.latitude) { +// northeast = coord1; +// southwest = coord2; +// } else { +// northeast = coord2; +// southwest = coord1; +// } + +// // Create the LatLngBounds object +// LatLngBounds bounds = +// LatLngBounds(northeast: northeast, southwest: southwest); + +// // Fit the camera to the bounds +// var cameraUpdate = CameraUpdate.newLatLngBounds(bounds, +// left: 180, top: 180, right: 180, bottom: 180); +// mapController!.animateCamera(cameraUpdate); +// update(); +// } + +// int selectedIndex = -1; // Initialize with no selection +// void selectCarFromList(int index) { +// selectedIndex = index; // Update selected index +// carTypes.forEach( +// (element) => element.isSelected = false); // Reset selection flags +// carTypes[index].isSelected = true; +// update(); +// } + +// showBottomSheet1() async { +// await bottomSheet(); +// isBottomSheetShown = true; +// heightBottomSheetShown = 250; + +// update(); +// } + +// final promo = TextEditingController(); +// bool promoTaken = false; +// void applyPromoCodeToPassenger(BuildContext context) async { +// if (promoTaken == true) { +// MyDialog().getDialog( +// 'Promo Already Used'.tr, +// 'You have already used this promo code.'.tr, +// () => Get.back(), +// ); +// return; +// } + +// if (!promoFormKey.currentState!.validate()) return; + +// // العتبات بالليرة السورية +// const double minPromoLowSYP = 172; // Speed / Balash +// const double minPromoHighSYP = 200; // Comfort / Electric / Lady + +// try { +// final value = await CRUD().get( +// link: AppLink.getPassengersPromo, +// payload: {'promo_code': promo.text}, +// ); + +// if (value == 'failure') { +// MyDialog().getDialog( +// 'Promo Ended'.tr, +// 'The promotion period has ended.'.tr, +// () => Get.back(), +// ); +// return; +// } + +// // هل يوجد فئة مؤهلة أصلاً قبل الخصم؟ +// final bool eligibleNow = (totalPassengerSpeed >= minPromoLowSYP) || +// (totalPassengerBalash >= minPromoLowSYP) || +// (totalPassengerComfort >= minPromoHighSYP) || +// (totalPassengerElectric >= minPromoHighSYP) || +// (totalPassengerLady >= minPromoHighSYP); + +// if (!eligibleNow) { +// Get.snackbar( +// 'Lowest Price Achieved'.tr, +// 'Cannot apply further discounts.'.tr, +// backgroundColor: AppColor.yellowColor, +// ); +// return; +// } + +// final decode = jsonDecode(value); +// if (decode["status"] != "success") { +// MyDialog().getDialog( +// 'Promo Ended'.tr, +// 'The promotion period has ended.'.tr, +// () => Get.back(), +// ); +// return; +// } + +// Get.snackbar('Promo Code Accepted'.tr, '', +// backgroundColor: AppColor.greenColor); + +// final firstElement = decode["message"][0]; +// final int discountPercentage = +// int.tryParse(firstElement['amount'].toString()) ?? 0; + +// // قيمة المحفظة - قد تكون سالبة +// final double walletVal = double.tryParse( +// box.read(BoxName.passengerWalletTotal)?.toString() ?? '0') ?? +// 0.0; + +// final bool isWalletNegative = walletVal < 0; + +// // -------------------------- +// // دالة تُطبّق الخصم دون النزول تحت الحد الأدنى +// // -------------------------- +// double _applyDiscountPerTier({ +// required double fare, +// required double minThreshold, +// required bool isWalletNegative, +// }) { +// if (fare < minThreshold) return fare; // غير مؤهل أصلاً + +// final double discount = fare * (discountPercentage / 100.0); +// double result; + +// if (isWalletNegative) { +// double neg = (-1) * walletVal; // walletVal < 0 => neg positive +// result = fare + neg - discount; +// } else { +// result = fare - discount; +// } + +// // لا نسمح بالنزول دون الحد الأدنى +// if (result < minThreshold) { +// result = minThreshold; +// } + +// // ولا نسمح بمبلغ سالب +// return result.clamp(0.0, double.infinity); +// } + +// // Comfort +// totalPassengerComfort = _applyDiscountPerTier( +// fare: totalPassengerComfort, +// minThreshold: minPromoHighSYP, +// isWalletNegative: isWalletNegative, +// ); + +// // Electric +// totalPassengerElectric = _applyDiscountPerTier( +// fare: totalPassengerElectric, +// minThreshold: minPromoHighSYP, +// isWalletNegative: isWalletNegative, +// ); + +// // Lady +// totalPassengerLady = _applyDiscountPerTier( +// fare: totalPassengerLady, +// minThreshold: minPromoHighSYP, +// isWalletNegative: isWalletNegative, +// ); + +// // Speed +// totalPassengerSpeed = _applyDiscountPerTier( +// fare: totalPassengerSpeed, +// minThreshold: minPromoLowSYP, +// isWalletNegative: isWalletNegative, +// ); + +// // Balash +// totalPassengerBalash = _applyDiscountPerTier( +// fare: totalPassengerBalash, +// minThreshold: minPromoLowSYP, +// isWalletNegative: isWalletNegative, +// ); + +// // تعديل دخل السائق وفق نسبة الخصم +// totalDriver = totalDriver - (totalDriver * discountPercentage / 100.0); + +// promoTaken = true; +// update(); + +// // مؤثرات +// Confetti.launch( +// context, +// options: const ConfettiOptions(particleCount: 100, spread: 70, y: 0.6), +// ); + +// Get.back(); +// await Future.delayed(const Duration(milliseconds: 120)); +// } catch (e) { +// Get.snackbar('Error'.tr, e.toString(), +// backgroundColor: AppColor.redColor); +// } +// } + +// double getDistanceFromText(String distanceText) { +// // Remove any non-digit characters from the distance text +// String distanceValue = distanceText.replaceAll(RegExp(r'[^0-9.]+'), ''); + +// // Parse the extracted numerical value as a double +// double distance = double.parse(distanceValue); + +// return distance; +// } + +// double costForDriver = 0; +// double totalPassengerSpeed = 0; +// double totalPassengerBalash = 0; +// double totalPassengerElectric = 0; +// double totalPassengerLady = 0; +// double totalPassengerRayehGai = 0; +// double totalPassengerRayehGaiComfort = 0; +// double totalPassengerRayehGaiBalash = 0; +// Future bottomSheet() async { +// // if (data.isEmpty) return; + +// // === إعدادات عامة === +// const double minFareSYP = 160; // حد أدنى +// const double minBillableKm = 0.3; // حد أدنى للمسافة المفوترة +// const double ladyFlatAddon = 20; // إضافة ثابتة لـ Lady +// const double airportAddonSYP = 200; // إضافة المطار + +// // --- ⬇️ الإضافة الجديدة: إضافة حدود مطار دمشق ⬇️ --- +// const double damascusAirportBoundAddon = 1400; // إضافة المطار (حدود) +// // --- ⬆️ نهاية الإضافة ⬆️ --- + +// // كهرباء +// const double electricPerKmUplift = 4; // زيادة/كم +// const double electricFlatAddon = 10; // زيادة ثابتة + +// // Long Speed +// const double longSpeedThresholdKm = 40.0; +// const double longSpeedPerKm = 26.0; // Speed عند >40كم + +// // قواعد الرحلات البعيدة للدقائق (تعمل لكل الأوقات) +// const double mediumDistThresholdKm = 25.0; // >25كم +// const double longDistThresholdKm = 35.0; // >35كم +// const double longTripPerMin = 6.0; +// const int minuteCapMedium = 60; // سقف دقائق عند >25كم +// const int minuteCapLong = 80; // سقف دقائق عند >35كم +// const int freeMinutesLong = 10; // عفو 10 دقائق عند >35كم + +// // تخفيضات المسافات الكبيرة للفئات غير Speed +// const double extraReduction100 = 0.07; // +7% فوق تخفيض >40كم للرحلات >100كم +// const double maxReductionCap = 0.35; // سقف 35% كحد أقصى + +// // ====== زمن الرحلة ====== +// durationToAdd = Duration(seconds: durationToRide); +// hours = durationToAdd.inHours; +// minutes = (durationToAdd.inMinutes % 60).round(); +// final DateTime currentTime = DateTime.now(); +// newTime = currentTime.add(durationToAdd); +// averageDuration = (durationToRide / 60) / distance; +// // +5 minutes per waypoint stop surcharge +// final int waypointSurchargeMinutes = activeMenuWaypointCount * 5; +// final int totalMinutes = +// (durationToRide / 60).floor() + waypointSurchargeMinutes; + +// // ====== أدوات مساعدة ====== +// bool _isAirport(String s) { +// final t = s.toLowerCase(); +// return t.contains('airport') || +// s.contains('مطار') || +// s.contains('المطار'); +// } + +// bool _isClub(String s) { +// final t = s.toLowerCase(); +// return t.contains('club') || +// t.contains('nightclub') || +// t.contains('night club') || +// s.contains('ديسكو') || +// s.contains('ملهى ليلي'); +// } + +// // --- ⬇️ الإضافة الجديدة: دالة التحقق من حدود المطار ⬇️ --- +// // (P1: 33.415313, 36.499687) (P2: 33.400265, 36.531505) +// bool _isInsideDamascusAirportBounds(double lat, double lng) { +// final double northLat = 33.415313; +// final double southLat = 33.400265; +// final double eastLng = 36.531505; +// final double westLng = 36.499687; + +// // التحقق من خط العرض (بين الشمال والجنوب) +// bool isLatInside = (lat <= northLat) && (lat >= southLat); +// // التحقق من خط الطول (بين الشرق والغرب) +// bool isLngInside = (lng <= eastLng) && (lng >= westLng); + +// return isLatInside && isLngInside; +// } +// // --- ⬆️ نهاية الإضافة ⬆️ --- + +// // أسعار الدقيقة من السيرفر +// final double naturePerMin = naturePrice; // طبيعي +// final double latePerMin = latePrice; // ليل +// final double heavyPerMin = heavyPrice; // ذروة + +// // سعر الدقيقة حسب الوقت (أساس قبل قواعد المسافة) +// double _perMinuteByTime(DateTime now, bool clubCtx) { +// final h = now.hour; +// if (h >= 21 || h < 1) return latePerMin; // ليل +// if (h >= 1 && h < 5) return clubCtx ? (latePerMin * 2) : latePerMin; +// if (h >= 14 && h <= 17) return heavyPerMin; // ذروة +// return naturePerMin; // طبيعي +// } + +// // حد أدنى +// double _applyMinFare(double fare) => +// (fare < minFareSYP) ? minFareSYP : fare; + +// // عمولة الراكب (kazan من السيرفر) +// double _withCommission(double base) => +// (base * (1 + kazan / 100)).ceilToDouble(); + +// // ====== سياق ====== +// final bool airportCtx = +// _isAirport(startNameAddress) || _isAirport(endNameAddress); +// final bool clubCtx = _isClub(startNameAddress) || _isClub(endNameAddress); + +// // --- ⬇️ الإضافة الجديدة: التحقق من سياق حدود المطار ⬇️ --- +// // !! ⚠️ تأكد من أن هذه هي المتغيرات الصحيحة لإحداثيات نقطة النهاية !! +// double destLat = 0.0; +// double destLng = 0.0; +// try { +// destLat = myDestination.latitude; +// destLng = myDestination.longitude; +// } catch (_) { +// if (coordinatesWithoutEmpty.isNotEmpty) { +// destLat = +// double.tryParse(coordinatesWithoutEmpty.last.split(',')[0]) ?? 0.0; +// destLng = +// double.tryParse(coordinatesWithoutEmpty.last.split(',')[1]) ?? 0.0; +// } +// } + +// final bool damascusAirportBoundCtx = _isInsideDamascusAirportBounds( +// destLat, +// destLng, +// ); +// final bool isInDamascusAirportBoundCtx = _isInsideDamascusAirportBounds( +// newMyLocation.latitude.toDouble(), // <-- ⚠️ غيّر هذا للمتغير الصحيح +// newMyLocation.longitude.toDouble(), // <-- ⚠️ غيّر هذا للمتغير الصحيح +// ); +// // --- ⬆️ نهاية الإضافة ⬆️ --- + +// // ====== مسافة مفوترة ====== +// final double billableDistance = +// (distance < minBillableKm) ? minBillableKm : distance; + +// // ====== Speed (قصير/طويل) ====== +// final bool isLongSpeed = billableDistance > longSpeedThresholdKm; +// final double perKmSpeedBaseFromServer = +// speedPrice; // مثال: 2900 يأتي من السيرفر +// final double perKmSpeed = +// isLongSpeed ? longSpeedPerKm : perKmSpeedBaseFromServer; + +// // ====== تخفيضات الفئات الأخرى حسب بُعد الرحلة ====== +// // ... (الكود كما هو) ... +// double reductionPct40 = 0.0; +// if (perKmSpeedBaseFromServer > 0) { +// reductionPct40 = (1.0 - (longSpeedPerKm / perKmSpeedBaseFromServer)) +// .clamp(0.0, maxReductionCap); +// } +// final double reductionPct100 = +// (reductionPct40 + extraReduction100).clamp(0.0, maxReductionCap); +// double distanceReduction = 0.0; +// if (billableDistance > 100.0) { +// distanceReduction = reductionPct100; +// } else if (billableDistance > 40.0) { +// distanceReduction = reductionPct40; +// } + +// // ====== منطق الدقيقة يعمل لكل الأوقات ويتكيّف مع المسافة ====== +// // ... (الكود كما هو) ... +// double effectivePerMin = _perMinuteByTime(currentTime, clubCtx); +// int billableMinutes = totalMinutes; +// if (billableDistance > longDistThresholdKm) { +// effectivePerMin = longTripPerMin; +// final int capped = +// (billableMinutes > minuteCapLong) ? minuteCapLong : billableMinutes; +// billableMinutes = capped - freeMinutesLong; +// if (billableMinutes < 0) billableMinutes = 0; +// } else if (billableDistance > mediumDistThresholdKm) { +// effectivePerMin = longTripPerMin; +// billableMinutes = (billableMinutes > minuteCapMedium) +// ? minuteCapMedium +// : billableMinutes; +// } + +// // ====== أسعار/كم قبل التخفيض ====== +// // ... (الكود كما هو) ... +// final double perKmComfortRaw = comfortPrice; +// final double perKmDelivery = deliveryPrice; +// final double perKmVanRaw = +// (familyPrice > 0 ? familyPrice : (speedPrice + 13)); +// final double perKmElectricRaw = perKmComfortRaw + electricPerKmUplift; + +// // ====== تطبيق التخفيضات على الفئات (نفس نسبة Speed للبعيد) ====== +// // ... (الكود كما هو) ... +// double perKmComfort = perKmComfortRaw * (1.0 - distanceReduction); +// double perKmElectric = perKmElectricRaw * (1.0 - distanceReduction); +// double perKmVan = perKmVanRaw * (1.0 - distanceReduction); +// perKmComfort = perKmComfort.clamp(0, double.infinity); +// perKmElectric = perKmElectric.clamp(0, double.infinity); +// perKmVan = perKmVan.clamp(0, double.infinity); +// final double perKmBalash = (perKmSpeed - 5).clamp(0, double.infinity); + +// // ====== دوال الاحتساب ====== +// double _oneWayFare({ +// required double perKm, +// required bool isLady, +// double flatAddon = 0, +// }) { +// double fare = billableDistance * perKm; +// fare += +// billableMinutes * effectivePerMin; // دقائق بعد السقف/العفو إن وُجد +// fare += flatAddon; +// if (isLady) fare += ladyFlatAddon; +// if (airportCtx) fare += airportAddonSYP; + +// // --- ⬇️ الإضافة الجديدة: تطبيق إضافة حدود المطار ⬇️ --- +// if (damascusAirportBoundCtx || isInDamascusAirportBoundCtx) { +// fare += damascusAirportBoundAddon; +// } +// // --- ⬆️ نهاية الإضافة ⬆️ --- + +// return _applyMinFare(fare); +// } + +// double _roundTripFare({required double perKm}) { +// // خصم 40% لمسافة إياب واحدة + زمن مضاعف (بنفس قواعد الدقيقة المعدّلة) +// double distPart = +// (billableDistance * 2 * perKm) - ((billableDistance * perKm) * 0.4); +// double timePart = (billableMinutes * 2) * effectivePerMin; +// double fare = distPart + timePart; +// if (airportCtx) fare += airportAddonSYP; + +// // --- ⬇️ الإضافة الجديدة: تطبيق إضافة حدود المطار ⬇️ --- +// // تنطبق أيضاً على رحلات الذهاب والعودة لأنها "تصل" إلى الوجهة +// if (damascusAirportBoundCtx || isInDamascusAirportBoundCtx) { +// fare += damascusAirportBoundAddon; +// } +// // --- ⬆️ نهاية الإضافة ⬆️ --- + +// return _applyMinFare(fare); +// } + +// // ====== حساب كل الفئات (Base قبل العمولة) ====== +// final double costSpeed = _oneWayFare(perKm: perKmSpeed, isLady: false); +// final double costBalash = _oneWayFare(perKm: perKmBalash, isLady: false); +// final double costComfort = _oneWayFare(perKm: perKmComfort, isLady: false); +// final double costElectric = _oneWayFare( +// perKm: perKmElectric, isLady: false, flatAddon: electricFlatAddon); +// final double costDelivery = +// _oneWayFare(perKm: perKmDelivery, isLady: false); +// final double costLady = _oneWayFare( +// perKm: perKmComfort, +// isLady: true); // Lady تعتمد Comfort بعد التخفيض + إضافة ثابتة +// final double costVan = _oneWayFare(perKm: perKmVan, isLady: false); +// final double costRayehGai = _roundTripFare(perKm: perKmSpeed); +// final double costRayehGaiComfort = _roundTripFare(perKm: perKmComfort); +// final double costRayehGaiBalash = _roundTripFare(perKm: perKmBalash); + +// // ====== أسعار الراكب بعد العمولة (kazan من السيرفر) ====== +// totalPassengerSpeed = _withCommission(costSpeed); +// totalPassengerBalash = _withCommission(costBalash); +// totalPassengerComfort = _withCommission(costComfort); +// totalPassengerElectric = _withCommission(costElectric); +// totalPassengerLady = _withCommission(costLady); +// totalPassengerScooter = _withCommission(costDelivery); +// totalPassengerVan = _withCommission(costVan); +// totalPassengerRayehGai = _withCommission(costRayehGai); +// totalPassengerRayehGaiComfort = _withCommission(costRayehGaiComfort); +// totalPassengerRayehGaiBalash = _withCommission(costRayehGaiBalash); + +// // افتراضي للعرض +// totalPassenger = totalPassengerSpeed; +// totalCostPassenger = totalPassenger; + +// // ====== دعم رصيد محفظة سلبي ====== +// try { +// final walletStr = box.read(BoxName.passengerWalletTotal).toString(); +// final walletVal = double.tryParse(walletStr) ?? 0.0; +// if (walletVal < 0) { +// final neg = (-1) * walletVal; +// totalPassenger += neg; +// totalPassengerComfort += neg; +// totalPassengerElectric += neg; +// totalPassengerLady += neg; +// totalPassengerBalash += neg; +// totalPassengerScooter += neg; +// totalPassengerRayehGai += neg; +// totalPassengerRayehGaiComfort += neg; +// totalPassengerRayehGaiBalash += neg; +// totalPassengerVan += neg; +// } +// } catch (e) { +// Log.print("Error: $e"); +// } + +// update(); +// changeBottomSheetShown(forceValue: true); +// } + +// List polylineCoordinate = []; +// String? cardNumber; +// void readyWayPoints() { +// hintTextwayPointStringAll = [ +// hintTextwayPoint0, +// hintTextwayPoint1, +// hintTextwayPoint2, +// hintTextwayPoint3, +// hintTextwayPoint4, +// ]; +// polylineCoordinatesPointsAll = [ +// polylineCoordinates0, +// polylineCoordinates1, +// polylineCoordinates2, +// polylineCoordinates3, +// polylineCoordinates4, +// ]; +// allTextEditingPlaces = [ +// wayPoint0Controller, +// wayPoint1Controller, +// wayPoint2Controller, +// wayPoint3Controller, +// wayPoint4Controller, +// ]; +// currentLocationToFormPlacesAll = [ +// currentLocationToFormPlaces0, +// currentLocationToFormPlaces1, +// currentLocationToFormPlaces2, +// currentLocationToFormPlaces3, +// currentLocationToFormPlaces4, +// ]; +// placeListResponseAll = [ +// wayPoint0, +// wayPoint1, +// wayPoint2, +// wayPoint3, +// wayPoint4 +// ]; +// startLocationFromMapAll = [ +// startLocationFromMap0, +// startLocationFromMap1, +// startLocationFromMap2, +// startLocationFromMap3, +// startLocationFromMap4, +// ]; +// currentLocationStringAll = [ +// currentLocationString0, +// currentLocationString1, +// currentLocationString2, +// currentLocationString3, +// currentLocationString4, +// ]; +// placesCoordinate = [ +// placesCoordinate0, +// placesCoordinate1, +// placesCoordinate2, +// placesCoordinate3, +// placesCoordinate4, +// ]; +// update(); +// } + +// List driversForMishwari = []; + +// Future selectDriverAndCarForMishwariTrip() async { +// // Calculate the bounds for 12km range +// double latitudeOffset = 0.1; // 20km range in latitude +// double longitudeOffset = 0.12; // 20km range in longitude + +// // Calculate bounding box based on passenger's location +// double southwestLat = passengerLocation.latitude - latitudeOffset; +// double northeastLat = passengerLocation.latitude + latitudeOffset; +// double southwestLon = passengerLocation.longitude - longitudeOffset; +// double northeastLon = passengerLocation.longitude + longitudeOffset; + +// // Create the payload with calculated bounds +// var payload = { +// 'southwestLat': southwestLat.toString(), +// 'northeastLat': northeastLat.toString(), +// 'southwestLon': southwestLon.toString(), +// 'northeastLon': northeastLon.toString(), +// }; + +// try { +// // Fetch data from the API +// var res = await CRUD().get( +// link: AppLink.selectDriverAndCarForMishwariTrip, payload: payload); + +// if (res != 'failure') { +// // Check if response is valid JSON +// try { +// var d = jsonDecode(res); +// driversForMishwari = d['message']; +// Log.print('driversForMishwari: $driversForMishwari'); +// update(); +// } catch (e) { +// // Handle invalid JSON format +// Log.print("Error decoding JSON: $e"); +// return 'Server returned invalid data. Please try again later.'; +// } +// } else { +// return 'No driver available now, try again later. Thanks for using our app.' +// .tr; +// } +// } catch (e) { +// // Handle network or other exceptions +// Log.print("Error fetching data: $e"); +// return 'There was an issue connecting to the server. Please try again later.' +// .tr; +// } +// } + +// final Rx selectedDateTime = DateTime.now().obs; + +// void updateDateTime(DateTime newDateTime) { +// selectedDateTime.value = newDateTime; +// } + +// Future mishwariOption() async { +// isLoading = true; +// update(); +// // add dialoug for select driver and car +// await selectDriverAndCarForMishwariTrip(); +// Future.delayed(Duration.zero); +// isLoading = false; +// update(); +// Get.to(() => CupertinoDriverListWidget()); + +// // changeCashConfirmPageShown(); +// } + +// var driverIdVip = ''; +// Future saveTripData( +// Map driver, DateTime tripDateTime) async { +// try { +// // Prepare trip data +// Map tripData = { +// 'id': driver['driver_id'].toString(), // Ensure the id is a string +// 'phone': driver['phone'], +// 'gender': driver['gender'], +// 'name': driver['NAME'], +// 'name_english': driver['name_english'], +// 'address': driver['address'], +// 'religion': driver['religion'] ?? 'UnKnown', +// 'age': driver['age'].toString(), // Convert age to String +// 'education': driver['education'] ?? 'UnKnown', //startlocationname +// 'license_type': driver['license_type'] ?? 'UnKnown', +// 'national_number': driver['national_number'] ?? 'UnKnown', +// 'car_plate': driver['car_plate'], +// 'make': driver['make'], +// 'model': driver['model'], +// 'year': driver['year'].toString(), // Convert year to String +// 'color': driver['color'], +// 'color_hex': driver['color_hex'], +// 'displacement': driver['displacement'], +// 'fuel': driver['fuel'], +// 'token': driver['token'], +// 'rating': driver['rating'].toString(), // Convert rating to String +// 'countRide': +// driver['ride_count'].toString(), // Convert countRide to String +// 'passengerId': box.read(BoxName.passengerID), +// 'timeSelected': tripDateTime.toIso8601String(), +// 'status': 'pending', +// 'startNameAddress': startNameAddress.toString(), +// 'locationCoordinate': +// '${data[0]["start_location"]['lat']},${data[0]["start_location"]['lng']}', +// }; +// Log.print('tripData: $tripData'); + +// // Send data to server +// var response = +// await CRUD().post(link: AppLink.addMishwari, payload: tripData); +// // Log.print('response: $response'); + +// if (response != 'failure') { +// // Trip saved successfully +// // Get.snackbar('Success'.tr, 'Trip booked successfully'.tr); +// var id = response['message']['id'].toString(); +// await CRUD() +// .post(link: '${AppLink.server}/ride/rides/add.php', payload: { +// "start_location": +// '${data[0]["start_location"]['lat']},${data[0]["start_location"]['lng']}', +// "end_location": +// '${data[0]["start_location"]['lat']},${data[0]["start_location"]['lng']}', +// "date": DateTime.now().toString(), +// "time": DateTime.now().toString(), +// "endtime": DateTime.now().add(const Duration(hours: 2)).toString(), +// "price": '50', +// "passenger_id": box.read(BoxName.passengerID).toString(), +// "driver_id": driver['driver_id'].toString(), +// "status": "waiting", +// 'carType': 'vip', +// "price_for_driver": '50', +// "price_for_passenger": '50', +// "distance": '20', +// "paymentMethod": 'cash', +// }).then((value) { +// if (value is String) { +// final parsedValue = jsonDecode(value); +// rideId = parsedValue['message']; +// } else if (value is Map) { +// rideId = value['message']; +// } else { +// Log.print('Unexpected response type: ${value.runtimeType}'); +// } +// }); + +// driverIdVip = driver['driver_id'].toString(); +// driverId = driver['driver_id'].toString(); + +// DateTime timeSelected = DateTime.parse(tripDateTime.toIso8601String()); +// Get.find().scheduleNotificationsForTimeSelected( +// "Your trip is scheduled".tr, +// "Don't forget your ride!".tr, +// "tone1", +// timeSelected); + +// await NotificationService.sendNotification( +// category: 'OrderVIP', +// target: driver['token'].toString(), +// title: 'OrderVIP'.tr, +// body: '$rideId - VIP Trip', +// isTopic: false, // Important: this is a token +// tone: 'tone1', +// driverList: [ +// id, +// rideId, +// driver['id'], +// passengerLocation.latitude.toString(), +// startNameAddress.toString(), +// passengerLocation.longitude.toString(), +// (box.read(BoxName.name).toString().split(' ')[0]).toString(), +// box.read(BoxName.passengerID).toString(), +// box.read(BoxName.phone).toString(), +// box.read(BoxName.email).toString(), +// box.read(BoxName.passengerPhotoUrl).toString(), +// box.read(BoxName.tokenFCM).toString(), +// (driver['token'].toString()), +// ], +// ); +// if (response['message'] == "Trip updated successfully") { +// mySnackbarSuccess("Trip updated successfully".tr); +// Log.print( +// 'previous_driver_token: ${response['previous_driver_token']}'); + +// await NotificationService.sendNotification( +// category: 'Order VIP Canceld', +// target: response['previous_driver_token'].toString(), +// title: 'Order VIP Canceld'.tr, +// body: 'Passenger cancel order'.tr, +// isTopic: false, // Important: this is a token +// tone: 'cancel', +// driverList: [], +// ); +// } +// // data = []; +// isBottomSheetShown = false; +// update(); +// Get.to(() => VipWaittingPage()); +// } else { +// throw Exception('Failed to save trip'); +// } +// } catch (e) { +// // Show error message with more details for debugging +// Get.snackbar('Error'.tr, 'Failed to book trip: $e'.tr, +// backgroundColor: AppColor.redColor); +// Log.print('Error: $e'); +// } +// } + +// Future cancelVip(String token, tripId) async { +// var res = await CRUD() +// .post(link: AppLink.cancelMishwari, payload: {'id': tripId}); +// if (res != 'failur') { +// Get.back(); +// mySnackbarSuccess('You canceled VIP trip'.tr); +// } +// } + +// void sendToDriverAgain(String token) { +// NotificationService.sendNotification( +// category: 'Order VIP Canceld', +// target: token.toString(), +// title: 'Order VIP Canceld'.tr, +// body: 'Passenger cancel order'.tr, +// isTopic: false, // Important: this is a token +// tone: 'cancel', +// driverList: [], +// ); +// } + +// // دالة الفحص عند بدء التطبيق +// Future detectAndCacheDeviceTier() async { +// // 1. استخدام الكلاس الذي أنشأناه سابقاً للفحص +// bool isHighEnd = await DevicePerformanceManager.isHighEndDevice(); + +// // 2. طباعة النتيجة للتأكد +// Log.print("Device Analysis - Is Flagship/HighEnd? $isHighEnd"); + +// // 3. تخزين النتيجة بشكل منطقي صحيح +// // إذا كان الجهاز قوياً (true)، فإن وضع الـ LowEnd يكون (false) +// // والعكس صحيح +// box.write(BoxName.lowEndMode, !isHighEnd); +// } + +// initilizeGetStorage() async { +// if (box.read(BoxName.addWork) == null) { +// box.write(BoxName.addWork, 'addWork'); +// } +// if (box.read(BoxName.addHome) == null) { +// box.write(BoxName.addHome, 'addHome'); +// } +// if (box.read(BoxName.lowEndMode) == null) { +// detectAndCacheDeviceTier(); +// } +// } + +// late List recentPlaces = []; + +// getFavioratePlaces() async { +// recentPlaces = await sql.getCustomQuery( +// 'SELECT * FROM ${TableName.recentLocations} ORDER BY createdAt DESC'); +// // Log.print('recentPlaces: ${recentPlaces}'); +// } + +// double passengerRate = 5; +// double comfortPrice = 45; +// double speedPrice = 40; +// double mashwariPrice = 40; +// double familyPrice = 55; +// double deliveryPrice = 1.2; +// double minFareSYP = 16000; // حد أدنى للأجرة (سوريا) +// double minBillableKm = 1.0; // حد أدنى للمسافة المفوترة +// double commissionPct = 15; // عمولة التطبيق % (راكب) + +// getKazanPercent() async { +// var res = await CRUD().get( +// link: AppLink.getKazanPercent, +// payload: {'country': box.read(BoxName.countryCode).toString()}, +// ); +// if (res != 'failure') { +// var json = jsonDecode(res); +// // التحقق الديناميكي من 'data' أو 'message' +// var dataList = json['data'] ?? json['message']; + +// if (dataList != null && dataList is List && dataList.isNotEmpty) { +// var firstRow = dataList[0]; +// kazan = double.parse(firstRow['kazan'].toString()); +// naturePrice = double.parse(firstRow['naturePrice'].toString()); +// heavyPrice = double.parse(firstRow['heavyPrice'].toString()); +// latePrice = double.parse(firstRow['latePrice'].toString()); +// comfortPrice = double.parse(firstRow['comfortPrice'].toString()); +// speedPrice = double.parse(firstRow['speedPrice'].toString()); +// deliveryPrice = double.parse(firstRow['deliveryPrice'].toString()); +// mashwariPrice = double.parse(firstRow['freePrice'].toString()); +// familyPrice = double.parse(firstRow['familyPrice'].toString()); +// fuelPrice = double.parse(firstRow['fuelPrice'].toString()); +// } +// } +// } + +// getPassengerRate() async { +// var res = await CRUD().get( +// link: AppLink.getPassengerRate, +// payload: {'passenger_id': box.read(BoxName.passengerID)}); +// if (res != 'failure') { +// var json = jsonDecode(res); +// var message = json['data'] ?? json['message']; +// if (message['rating'] == null) { +// passengerRate = 5.0; // Default rating +// } else { +// // Safely parse the rating to double +// var rating = message['rating']; +// if (rating is String) { +// passengerRate = +// double.tryParse(rating) ?? 5.0; // Default if parsing fails +// } else if (rating is num) { +// passengerRate = +// rating.toDouble(); // Already a number, convert to double +// } else { +// passengerRate = 5.0; // Default for unexpected data types +// } +// } +// } else { +// passengerRate = 5.0; // Default rating for failure +// } +// } + +// addFingerPrint() async { +// String fingerPrint = await DeviceHelper.getDeviceFingerprint(); +// await CRUD().postWallet(link: AppLink.addFingerPrint, payload: { +// 'token': (box.read(BoxName.tokenFCM.toString())), +// 'passengerID': box.read(BoxName.passengerID).toString(), +// "fingerPrint": fingerPrint +// }); +// } + +// firstTimeRunToGetCoupon() async { +// // Check if it's the first time and the app is installed and gift token is available +// if (box.read(BoxName.isFirstTime).toString() == '0' && +// box.read(BoxName.isInstall).toString() == '1' && +// box.read(BoxName.isGiftToken).toString() == '0') { +// var promo, discount, validity; +// var resPromo = await CRUD().get(link: AppLink.getPromoFirst, payload: { +// "passengerID": box.read(BoxName.passengerID).toString(), +// }); +// if (resPromo != 'failure') { +// var d1 = jsonDecode(resPromo); +// promo = d1['message']['promo_code']; +// discount = d1['message']['amount']; +// validity = d1['message']['validity_end_date']; +// } +// box.write(BoxName.isFirstTime, '1'); + +// // Show a full-screen modal styled as an ad +// Get.dialog( +// AlertDialog( +// contentPadding: +// EdgeInsets.zero, // Removes the padding around the content +// content: SizedBox( +// width: 300, // Match the width of PromoBanner +// // height: 250, // Match the height of PromoBanner +// child: PromoBanner( +// promoCode: promo, +// discountPercentage: discount, +// validity: validity, +// ), +// ), +// ), +// ); +// } +// } + +// // --- دالة جديدة للاستماع ومعالجة الرابط --- +// void _listenForDeepLink() { +// ever(_deepLinkController.rawDeepLink, (String? link) async { +// if (link != null && link.isNotEmpty) { +// Log.print('📍 MapPassengerController processing link: $link'); + +// // 1. استخراج الإحداثيات باستخدام الدالة الموجودة لديك مسبقاً +// Map? coordinates = +// await extractCoordinatesFromLinkAsync(link); + +// if (coordinates != null) { +// double destLat = coordinates['latitude']!; +// double destLng = coordinates['longitude']!; +// myDestination = LatLng(destLat, destLng); + +// // 2. التحقق من موقع الراكب الحالي +// if (passengerLocation == null || +// (passengerLocation.latitude == 0 && +// passengerLocation.longitude == 0)) { +// Log.print('⏳ Waiting for current location to calculate route...'); +// await getLocation(); // جلب موقع الراكب إذا لم يكن متاحاً +// } + +// if (passengerLocation != null) { +// String originStr = +// '${passengerLocation.latitude},${passengerLocation.longitude}'; +// String destStr = '$destLat,$destLng'; + +// Log.print( +// '🚀 Drawing route from Deep Link: $originStr to $destStr'); + +// // 3. مسح أي مسارات ونقاط توقف سابقة +// clearPolyline(); +// waypoints.clear(); +// clearAllMenuWaypoints(); + +// // 4. استدعاء دالة رسم المسار وحساب التكلفة التي برمجتها +// await getDirectionMap(originStr, destStr); + +// // 5. إظهار الواجهة السفلية للرحلة ليكون الطلب جاهزاً بنقرة واحدة +// isBottomSheetShown = true; +// heightBottomSheetShown = 250; +// update(); + +// Get.snackbar( +// 'Location Received'.tr, +// 'Route and prices have been calculated successfully!'.tr, +// backgroundColor: AppColor.greenColor, +// colorText: Colors.white, +// ); +// } +// } else { +// Log.print('⚠️ Could not extract valid coordinates from link: $link'); +// } + +// // تفريغ الرابط بعد معالجته حتى لا يتم استدعاؤه مرة أخرى بالخطأ +// _deepLinkController.rawDeepLink.value = null; +// } +// }); + +// // معالجة الرابط إذا كان موجوداً مسبقاً (Cold Start) قبل تفعيل المستمع +// if (_deepLinkController.rawDeepLink.value != null && +// _deepLinkController.rawDeepLink.value!.isNotEmpty) { +// String link = _deepLinkController.rawDeepLink.value!; +// _deepLinkController.rawDeepLink.value = null; + +// // نؤجل التنفيذ قليلاً لضمان تحميل الخريطة +// Future.delayed(const Duration(milliseconds: 500), () async { +// Log.print( +// '📍 MapPassengerController processing link (Cold Start): $link'); + +// Map? coordinates = +// await extractCoordinatesFromLinkAsync(link); + +// if (coordinates != null) { +// double destLat = coordinates['latitude']!; +// double destLng = coordinates['longitude']!; +// myDestination = LatLng(destLat, destLng); + +// if (passengerLocation == null || +// (passengerLocation.latitude == 0 && +// passengerLocation.longitude == 0)) { +// await getLocation(); +// } + +// if (passengerLocation != null) { +// String originStr = +// '${passengerLocation.latitude},${passengerLocation.longitude}'; +// String destStr = '$destLat,$destLng'; + +// clearPolyline(); +// waypoints.clear(); +// clearAllMenuWaypoints(); +// await getDirectionMap(originStr, destStr); + +// isBottomSheetShown = true; +// heightBottomSheetShown = 250; +// update(); +// } +// } +// }); +// } +// } + +// @override +// void onInit() async { +// super.onInit(); +// _checkAndRefreshMapStyle(); // Verify style version and clear cache if needed +// // // --- إضافة جديدة: تهيئة وحدة التحكم في الروابط العميقة --- +// Get.put(DeepLinkController(), permanent: true); +// // // ---------------------------------------------------- +// // مرحلة 0: الضروري جداً لعرض الخريطة سريعاً +// // mapAPIKEY = await storage.read(key: BoxName.mapAPIKEY); +// await initilizeGetStorage(); // إعداد سريع +// await _initMinimalIcons(); // start/end فقط +// // await addToken(); // لو لازم للمصادقة +// _listenForDeepLink(); +// // initSocket(); +// await getLocation(); // لتحديد الكاميرا +// box.write(BoxName.carType, 'yet'); +// box.write(BoxName.tipPercentage, '0'); +// // await detectAndCacheDeviceTier(); + +// // لا تُنشئ Controllers الثقيلة الآن: +// Get.lazyPut(() => TextToSpeechController(), +// fenix: true); +// Get.lazyPut(() => FirebaseMessagesController(), +// fenix: true); +// Get.lazyPut(() => AudioRecorderController(), +// fenix: true); + +// // ابدأ الخريطة الآن (الشاشة ظهرت للمستخدم) +// Future.delayed(const Duration(seconds: 4), () { +// if (isLoading) { +// isLoading = false; +// update(); +// } +// }); + +// // مرحلة 1: مهام ضرورية للتسعير لكن غير حرجة لظهور UI +// unawaited(_stagePricingAndState()); + +// // مرحلة 2: تحسينات/كماليات بالخلفية +// unawaited(_stageNiceToHave()); + +// // ابدأ إعادة تحميل الماركر لكن بثروتل داخلي +// // startMarkerReloading(); // تأكد أنه مَخنوق التحديث (throttled) +// _startMasterTimer(); + +// // Start listening to emergency shake gestures +// EmergencySignalService.instance.startListening(() { +// if (statusRide == 'Begin' || statusRide == 'start') { +// Log.print("🚨 Emergency shake verified! Prompting SOS..."); +// if (isBottomSheetShown) { +// sosPassenger(); +// } else { +// Get.snackbar( +// 'Emergency Mode Triggered'.tr, +// 'Stay calm. We are here to help.'.tr, +// backgroundColor: AppColor.redColor, +// colorText: Colors.white, +// duration: const Duration(seconds: 4), +// ); +// sosPassenger(); +// } +// } +// }); +// } + +// // === Helpers === + +// Future _initMinimalIcons() async { +// // Icons are now loaded dynamically via MapLibre's _loadMapIcons onStyleLoaded +// } + +// Future _stagePricingAndState() async { +// try { +// await getKazanPercent(); // أسعار السيرفر +// } catch (e) { +// Log.print("Error: $e"); +// } +// try { +// _checkInitialRideStatus(); // تحقق من حالة الرحلة الحالية +// } catch (e) { +// Log.print("Error: $e"); +// } +// // لو عندك ضبط “وضع خفيف” حسب الجهاز: +// _applyLowEndModeIfNeeded(); +// } + +// Future _stageNiceToHave() async { +// Log.print('🚀 MapPassengerController: Starting _stageNiceToHave'); + +// // 🔥 Fix: Future.wait uses ONE argument (the list). +// await Future.wait([ +// Future(() async { +// try { +// Log.print('🔍 Loading Favorites...'); +// getFavioratePlaces(); +// } catch (e) { +// Log.print("Error: $e"); +// } +// }), +// Future(() async { +// try { +// Log.print('🔍 Loading Waypoints...'); +// readyWayPoints(); +// } catch (e) { +// Log.print("Error: $e"); +// } +// }), +// Future(() async { +// try { +// Log.print('🔍 Loading Rate...'); +// getPassengerRate(); +// } catch (e) { +// Log.print("Error: $e"); +// } +// }), +// Future(() async { +// try { +// Log.print('🔍 Loading Coupons...'); +// firstTimeRunToGetCoupon(); +// } catch (e) { +// Log.print("Error: $e"); +// } +// }), +// ]); +// Log.print('✅ MapPassengerController: _stageNiceToHave complete'); +// try { +// cardNumber = await SecureStorage().readData(BoxName.cardNumber); +// } catch (e) { +// Log.print("Error: $e"); +// } +// } + +// void _applyLowEndModeIfNeeded() { +// // مثال بسيط: يمكنك حفظ فلاج بنظامك (من السيرفر/الإعدادات/الكاش) لتفعيل وضع خفيف +// // لاحقاً فعّل: map.trafficEnabled=false, buildingsEnabled=false, تبسيط polylines... +// // controller.lowEndMode = true; +// } + +// uploadPassengerLocation() async { +// await CRUD().post(link: AppLink.addpassengerLocation, payload: { +// "passengerId": box.read(BoxName.passengerID), +// "lat": passengerLocation.latitude.toString(), +// "lng": passengerLocation.longitude.toString(), +// "rideId": rideId.toString() +// }); +// } + +// void _showRideStartNotifications() { +// // تنبيهات الأسعار حسب نوع السيارة +// if (['Speed', 'Awfar Car'].contains(box.read(BoxName.carType))) { +// NotificationController().showNotification('Fixed Price'.tr, +// 'The captain is responsible for the route.'.tr, 'ding'); +// } else if (['Comfort', 'Lady'].contains(box.read(BoxName.carType))) { +// NotificationController().showNotification('Attention'.tr, +// 'The price may increase if the route changes.'.tr, 'ding'); +// } +// } + +// /// Checks the current version of assets/style.json and purges the map cache if it has changed. +// Future _checkAndRefreshMapStyle() async { +// try { +// final String styleJson = await rootBundle.loadString('assets/style.json'); +// final Map decoded = json.decode(styleJson); +// final String? currentVersion = +// decoded['metadata'] != null ? decoded['metadata']['version'] : null; + +// if (currentVersion == null) return; + +// final String lastVersion = box.read(BoxName.styleVersion) ?? "0.0.0"; + +// if (currentVersion != lastVersion) { +// Log.print( +// "♻️ Map Style Version mismatch ($lastVersion -> $currentVersion). Purging offline cache..."); +// await OfflineMapService.instance.clearCache(); + +// // Final verification check: give native engine time to flush +// await Future.delayed(const Duration(milliseconds: 500)); + +// box.write(BoxName.styleVersion, currentVersion); +// Log.print("✅ Style Version updated to $currentVersion"); +// } +// } catch (e) { +// Log.print("⚠️ Style version check failed: $e"); +// } +// } +// } + +// class CarLocation { +// final String id; +// final double latitude; +// final double longitude; +// final double distance; +// final double duration; + +// CarLocation({ +// required this.id, +// required this.latitude, +// required this.longitude, +// this.distance = 10000, +// this.duration = 10000, +// }); +// } diff --git a/lib/controller/home/points_for_rider_controller.dart b/lib/controller/home/points_for_rider_controller.dart index 54049b0..e9ef9f1 100644 --- a/lib/controller/home/points_for_rider_controller.dart +++ b/lib/controller/home/points_for_rider_controller.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; -import 'package:Intaleq/constant/style.dart'; -import 'package:Intaleq/controller/home/map_passenger_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart'; import '../../constant/api_key.dart'; import '../../constant/links.dart'; +import '../../constant/style.dart'; import '../functions/crud.dart'; import '../functions/location_controller.dart'; @@ -114,14 +114,21 @@ class WayPointController extends GetxController { void onInit() { // Get.put(LocationController()); addWayPoints(); - myLocation = Get.find().passengerLocation; + myLocation = Get.find().passengerLocation; super.onInit(); } + + void reset() { + wayPoints.clear(); + addWayPoints(); + placeListResponse.clear(); + update(); + } } class PlaceList extends StatelessWidget { // Get the controller instance - final controller = Get.put(WayPointController()); + final controller = Get.find(); @override Widget build(BuildContext context) { diff --git a/lib/controller/home/precise_comparison_results.txt b/lib/controller/home/precise_comparison_results.txt new file mode 100644 index 0000000..9b99a02 --- /dev/null +++ b/lib/controller/home/precise_comparison_results.txt @@ -0,0 +1,159 @@ +--- PRECISE MISSING METHODS --- +Total original methods/getters/setters: 270 +Total defined in split controllers: 270 +Total missing: 53 + - Column + - CupertinoDialogAction + - Future + - _applyLowEndModeIfNeeded + - _buildOsrmWaypointCoords + - _calculateDistance + - _checkAndRecalculateIfDeviated + - _fillDriverDataLocally + - _haversineKm + - _initMinimalIcons + - _initializePolygons + - _isActiveRideState + - _kmToLatDelta + - _kmToLngDelta + - _onDriverAcceptedWithSocket + - _onDriverArrivedWithSocket + - _onRideCancelledWithSocket + - _onRideStartedWithSocket + - _playRouteAnimation + - _relevanceScore + - _restorePolyline + - _retryProcess + - _stageNiceToHave + - _stagePricingAndState + - _startMasterTimer + - _startMasterTimerWithInterval + - _startPollingFallback + - _stopDriverLocationPolling + - _updateDriverMarker + - addPostFrameCallback + - cancelRide + - checkPassengerLocation + - currentDriverMarkerId + - detectPerfMode + - directions + - getAIKey + - getDirectionMap + - getDistanceFromDriverAfterAcceptedRide + - getMapPointsForAllMethods + - getPassengerLocationUniversity + - getRideStatus + - handleActiveRideOnStartup + - handleNoDriverFound + - isDriversDataValid + - lastWhere + - onChangedPassengerCount + - onChangedPassengersChoose + - processRideAcceptance + - retrySearchForDrivers + - showDrawingBottomSheet + - showNoDriversDialog + - startSearchingTimer + - wait + +--- PRECISE MISSING VARIABLES/FIELDS --- +Total original variables: 626 +Total defined in split controllers: 558 +Total missing: 97 + - EdgeInsets + - Error + - InfoWindow + - LatLngBounds + - LocationData + - R + - _buildOsrmWaypointCoords + - _calculateDistance + - _haversineKm + - _isActiveRideState + - _isStateProcessing + - _isUsingFallback + - _kmToLatDelta + - _kmToLngDelta + - _reconnectTimer + - _relevanceScore + - a + - aerialDistance + - apiDistanceMeters + - apiKey + - attemptCount + - c + - carInfo + - cardNumber + - carsOrder + - checkPassengerLocation + - commissionPct + - context + - coordDestination + - currentAttempt + - currentCarType + - currentLocationOfDrivers + - currentRideId + - currentTimeSearchingCaptainWindow + - dInfo + - dLat + - dataCarsLocationByPassenger + - datadriverCarsLocationToPassengerAfterApplied + - dest + - deviation + - distanceOfTrip + - driverCarPlate + - driverLocationToPassenger + - driverName + - driverOrderStatus + - driverPhone + - durationByPassenger + - dynamicApiUrl + - etaText + - fName + - finalReason + - firebaseMessagesController + - increaseFeeFormKey + - info + - isBeginRideFromDriverRunning + - isDrawingRoute + - isDriversDataValid + - isDriversTokensSend + - isInUniversity + - isRequestValid + - kDurationScalar + - key + - km + - kmInDegree + - lName + - latDest + - latestWaypoint + - lngDest + - lowPerf + - mapAPIKEY + - messagesFormKey + - might + - minBillableKm + - minFareSYP + - newValue + - northeast + - originCoords + - pLower + - passengerLocation + - passengerLocationStringUnvirsity + - placeName + - polylineString + - previousLocationOfDrivers + - progressTimerRideBeginVip + - promoFormKey + - qLower + - query + - rLat1 + - rLat2 + - ram + - rideData + - sdk + - selectedPassengerCount + - southwest + - startLng + - status + - stringElapsedTimeRideBegin diff --git a/lib/controller/home/profile/complaint_controller.dart b/lib/controller/home/profile/complaint_controller.dart index 1f3e0d2..ef05eb6 100644 --- a/lib/controller/home/profile/complaint_controller.dart +++ b/lib/controller/home/profile/complaint_controller.dart @@ -85,11 +85,13 @@ class ComplaintController extends GetxController { var uri = Uri.parse('${AppLink.server}/upload_audio.php'); var request = http.MultipartRequest('POST', uri); String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]; + final String fingerPrint = box.read(BoxName.deviceFpEncrypted)?.toString() ?? ''; var mimeType = lookupMimeType(audioFile.path); // ** التعديل: تم استخدام نفس هيدر التوثيق ** request.headers.addAll({ 'Authorization': 'Bearer $token', + 'X-Device-FP': fingerPrint, }); request.files.add( await http.MultipartFile.fromPath( diff --git a/lib/controller/home/vip_waitting_page.dart b/lib/controller/home/vip_waitting_page.dart index adf873a..5708ed7 100644 --- a/lib/controller/home/vip_waitting_page.dart +++ b/lib/controller/home/vip_waitting_page.dart @@ -4,8 +4,7 @@ import 'dart:convert'; import 'package:Intaleq/constant/box_name.dart'; import 'package:Intaleq/constant/colors.dart'; import 'package:Intaleq/constant/links.dart'; -import 'package:Intaleq/constant/style.dart'; -import 'package:Intaleq/controller/home/map_passenger_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart'; import 'package:Intaleq/main.dart'; import 'package:Intaleq/views/widgets/elevated_btn.dart'; import 'package:Intaleq/views/widgets/my_scafold.dart'; @@ -15,6 +14,7 @@ import 'package:flutter_font_icons/flutter_font_icons.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; +import '../../constant/style.dart'; import '../functions/crud.dart'; import '../functions/encrypt_decrypt.dart'; @@ -42,7 +42,7 @@ class VipOrderController extends GetxController { Future fetchOrder() async { try { isLoading.value = true; - var mapPassengerController = Get.find(); + var mapPassengerController = Get.find(); var res = await CRUD().get( link: AppLink.getMishwari, @@ -239,7 +239,7 @@ class VipWaittingPage extends StatelessWidget { title: "Cancel Trip".tr, kolor: AppColor.redColor, onPressed: () { - Get.find().cancelVip( + Get.find().cancelVip( data['token'].toString(), data['id'].toString(), ); @@ -272,7 +272,7 @@ class VipWaittingPage extends StatelessWidget { title: "Send to Driver Again".tr, kolor: AppColor.greenColor, onPressed: () { - Get.find() + Get.find() .sendToDriverAgain(data['token']); vipOrderController.fetchOrder(); }, @@ -292,7 +292,7 @@ class VipWaittingPage extends StatelessWidget { kolor: AppColor.greenColor, onPressed: () { final mapPassengerController = - Get.find(); + Get.find(); mapPassengerController.make = data['make']; mapPassengerController.licensePlate = data['car_plate']; diff --git a/lib/controller/local/translations.dart b/lib/controller/local/translations.dart index 0571c9a..5244054 100644 --- a/lib/controller/local/translations.dart +++ b/lib/controller/local/translations.dart @@ -24,6 +24,7 @@ class MyTranslation extends Translations { "4 Passengers": "٤ ركاب", "2. Attach Recorded Audio (Optional)": "٢. إرفاق تسجيل صوتي (اختياري)", + "Accept": "قبول", "Account": "الحساب", "Actions": "الإجراءات", "Active Users": "المستخدمين النشطين", @@ -41,9 +42,12 @@ class MyTranslation extends Translations { "Arrived": "وصلنا", "Audio Recording": "تسجيل صوتي", "Call": "اتصال", + "Call Connected": "تم فتح الاتصال", "Call Support": "اتصل بالدعم", "Call left": "مكالمات متبقية", + "Calling": "عم نتصل بـ", "Change Photo": "تغيير الصورة", + "Captain": "الكابتن", "Choose from Gallery": "اختر من المعرض", "Choose from contact": "اختر من جهات الاتصال", "Click to track the trip": "اضغط لتتبع المشوار", @@ -52,11 +56,13 @@ class MyTranslation extends Translations { "Complete Payment": "إتمام الدفع", "Confirm Cancellation": "تأكيد الإلغاء", "Confirm Pickup Location": "تأكيد موقع الانطلاق", + "Connecting...": "عم يتم الاتصال...", "Connection failed. Please try again.": "فشل الاتصال. جرب مرة تانية.", "Could not create ride. Please try again.": "ما قدرنا نعمل مشوار. جرب مرة تانية.", "Crop Photo": "قص الصورة", "Dark Mode": "الوضع الليلي", + "Decline": "رفض", "Delete": "حذف", "Delete Account": "حذف الحساب", "Delete All": "حذف الكل", @@ -73,6 +79,7 @@ class MyTranslation extends Translations { "Driver is Going To You": "الكابتن جاي لعندك", "Emergency Mode Triggered": "تم تفعيل وضع الطوارئ", "Emergency SOS": "طوارئ SOS", + "End": "إنهاء", "Enter the 5-digit code": "أدخل الكود المكون من ٥ أرقام", "Enter your City": "أدخل مدينتك", "Enter your Password": "أدخل كلمة السر", @@ -84,6 +91,7 @@ class MyTranslation extends Translations { "Failed to upload photo": "فشل رفع الصورة", "Finished": "انتهى", "Fixed Price": "سعر ثابت", + "Free Call": "مكالمة مجانية", "General": "عام", "Grant": "منح الإذن", "Have a Promo Code?": "معك كود خصم؟", @@ -109,6 +117,7 @@ class MyTranslation extends Translations { "Logout": "تسجيل خروج", "Map Error": "خطأ بالخريطة", "Message": "رسالة", + "Mute": "كتم الصوت", "Move map to select destination": "حرك الخريطة لتحديد الوجهة", "Move map to set start location": "حرك الخريطة لتحديد نقطة البداية", "Move map to set stop": "حرك الخريطة لتحديد التوقف", @@ -205,6 +214,7 @@ class MyTranslation extends Translations { "Share your experience to help us improve...": "شاركنا تجربتك لنحسن خدمتنا...", "Something went wrong. Please try again.": "صار غلط. جرب مرة تانية.", + "Speaker": "مكبر الصوت", "Speaking...": "عم يحكي...", "Speed Over": "تجاوز السرعة", "Start Point": "نقطة البداية", @@ -254,6 +264,7 @@ class MyTranslation extends Translations { "cancelled": "تكنسل", "due to a previous trip.": "عن مشوار قديم.", "insert sos phone": "دخل رقم طوارئ", + "is calling you": "عم يتصل فيك", "is driving a": "عم يسوق", "min added to fare": "دقيقة نضافت للأجرة", "phone not verified": "رقم الموبايل مو متأكد", diff --git a/lib/controller/payment/payment_controller.dart b/lib/controller/payment/payment_controller.dart index 2c5ddc9..3b6179b 100644 --- a/lib/controller/payment/payment_controller.dart +++ b/lib/controller/payment/payment_controller.dart @@ -10,7 +10,9 @@ import 'package:flutter_paypal/flutter_paypal.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; import 'package:get/get.dart'; import 'package:local_auth/local_auth.dart'; -import 'package:Intaleq/controller/home/map_passenger_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_state.dart'; +import 'package:Intaleq/controller/home/map/ride_state.dart'; import 'package:webview_flutter/webview_flutter.dart'; import '../../constant/box_name.dart'; @@ -34,7 +36,7 @@ class PaymentController extends GetxController { final formKey = GlobalKey(); final promo = TextEditingController(); final walletphoneController = TextEditingController(); - double totalPassenger = Get.find().totalPassenger; + double totalPassenger = Get.find().totalPassenger; int? selectedAmount = 0; List totalPassengerWalletDetails = []; String passengerTotalWalletAmount = ''; @@ -79,7 +81,7 @@ class PaymentController extends GetxController { Future generateTokenDriver(String amount) async { var res = await CRUD().post(link: AppLink.addPaymentTokenDriver, payload: { - 'driverID': Get.find().driverId, + 'driverID': Get.find().driverId, 'amount': amount.toString(), }); var d = jsonDecode(res); @@ -121,7 +123,7 @@ class PaymentController extends GetxController { 'payment_method': 'cancel-from-near', 'passengerID': box.read(BoxName.passengerID).toString(), 'token': paymentTokenWait, - 'driverID': Get.find().driverId.toString(), + 'driverID': Get.find().driverId.toString(), }); var paymentTokenWait1 = await generateTokenDriver(costOfWaiting5Minute.toString()); @@ -131,7 +133,7 @@ class PaymentController extends GetxController { 'amount': (costOfWaiting5Minute).toStringAsFixed(0), 'paymentMethod': 'cancel-from-near', 'token': paymentTokenWait1, - 'driverID': Get.find().driverId.toString(), + 'driverID': Get.find().driverId.toString(), }); if (res != 'failure') { @@ -139,13 +141,13 @@ class PaymentController extends GetxController { // 'Cancel', // 'Trip Cancelled. The cost of the trip will be added to your wallet.' // .tr, - // Get.find().driverToken, + // Get.find().driverToken, // [], // 'cancel', // ); await NotificationService.sendNotification( category: 'Cancel', - target: Get.find().driverToken.toString(), + target: Get.find().driverToken.toString(), title: 'Cancel'.tr, body: 'Trip Cancelled. The cost of the trip will be added to your wallet.' @@ -226,7 +228,7 @@ class PaymentController extends GetxController { var firstElement = decod["message"][0]; totalPassenger = totalPassenger - (totalPassenger * int.parse(firstElement['amount'])); - MapPassengerController().promoTaken = true; + Get.find().promoTaken = true; update(); } }); diff --git a/lib/controller/rate/rate_conroller.dart b/lib/controller/rate/rate_conroller.dart index 42f3574..e3f8d54 100644 --- a/lib/controller/rate/rate_conroller.dart +++ b/lib/controller/rate/rate_conroller.dart @@ -4,7 +4,7 @@ import 'package:Intaleq/constant/box_name.dart'; import 'package:Intaleq/constant/links.dart'; import 'package:Intaleq/constant/style.dart'; import 'package:Intaleq/controller/functions/crud.dart'; -import 'package:Intaleq/controller/home/map_passenger_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart'; import 'package:Intaleq/main.dart'; import 'package:Intaleq/views/home/map_page_passenger.dart'; import 'package:Intaleq/views/widgets/elevated_btn.dart'; @@ -45,14 +45,14 @@ class RateController extends GetxController { confirm: MyElevatedButton(title: 'Ok', onPressed: () => Get.back())); } else if (Get.find().isWalletChecked == true) { double tip = 0; - tip = (Get.find().totalPassenger) * + tip = (Get.find().totalPassenger) * (double.parse(box.read(BoxName.tipPercentage).toString())); if (tip > 0) { var res = await CRUD().post(link: AppLink.addTips, payload: { 'passengerID': box.read(BoxName.passengerID), - 'driverID': Get.find().driverId.toString(), - 'rideID': Get.find().rideId.toString(), + 'driverID': Get.find().driverId.toString(), + 'rideID': Get.find().rideId.toString(), 'tipAmount': tip.toString(), }); await Get.find() @@ -62,8 +62,8 @@ class RateController extends GetxController { ? tip.toStringAsFixed(0) : (tip * 100).toString()); await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: { - 'driverID': Get.find().driverId.toString(), - 'paymentID': '${Get.find().rideId}tip', + 'driverID': Get.find().driverId.toString(), + 'paymentID': '${Get.find().rideId}tip', 'amount': box.read(BoxName.countryCode) == 'Egypt' ? tip.toStringAsFixed(0) : (tip * 100).toString(), @@ -73,10 +73,10 @@ class RateController extends GetxController { if (res != 'failure') { await NotificationService.sendNotification( category: 'You Have Tips', - target: Get.find().driverToken.toString(), + target: Get.find().driverToken.toString(), title: 'You Have Tips'.tr, body: - '${'${tip.toString()}\$${' tips\nTotal is'.tr}'} ${tip + (Get.find().totalPassenger)}', + '${'${tip.toString()}\$${' tips\nTotal is'.tr}'} ${tip + (Get.find().totalPassenger)}', isTopic: false, // Important: this is a token tone: 'ding', driverList: [], @@ -95,7 +95,7 @@ class RateController extends GetxController { }, ); - Get.find().restCounter(); + Get.find().restCounter(); Get.offAll(const MapPagePassenger()); } } diff --git a/lib/controller/voice_call_controller.dart b/lib/controller/voice_call_controller.dart new file mode 100644 index 0000000..f8a1667 --- /dev/null +++ b/lib/controller/voice_call_controller.dart @@ -0,0 +1,722 @@ +import 'dart:async'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc; +import 'package:get/get.dart' hide Response; +import 'package:permission_handler/permission_handler.dart'; +import 'package:just_audio/just_audio.dart'; + +import '../../constant/box_name.dart'; +import '../../constant/links.dart'; +import '../../main.dart'; +import '../../print.dart'; +import '../../services/signaling_service.dart'; +import '../../views/widgets/voice_call_bottom_sheet.dart'; +import 'functions/crud.dart'; + +// EN: Enum representing the different states of a voice call. +// AR: تعداد يمثل الحالات المختلفة للمكالمة الصوتية. +enum VoiceCallState { idle, dialing, ringing, connecting, active, ended } + +class VoiceCallController extends GetxController with WidgetsBindingObserver { + // EN: Instance of the signaling service to manage WebSocket communication. + // AR: مثيل لخدمة الإشارات لإدارة الاتصال عبر الـ WebSocket. + final SignalingService _signaling = SignalingService(); + + // --- Observable Variables (GetX) / المتغيرات التفاعلية --- + + // EN: Current state of the call. + // AR: الحالة الحالية للمكالمة. + var state = VoiceCallState.idle.obs; + + // EN: Unique identifier for the WebRTC session. + // AR: المعرف الفريد لجلسة الاتصال. + var sessionId = "".obs; + + // EN: ID of the current active ride. + // AR: معرف الرحلة النشطة الحالية. + var rideId = "".obs; + + // EN: Name of the other party (Driver/Passenger). + // AR: اسم الطرف الآخر في المكالمة (سائق/راكب). + var remoteName = "User".obs; + + // EN: Microphone mute status. + // AR: حالة كتم الميكروفون. + var isMuted = false.obs; + + // EN: Speakerphone status. + // AR: حالة مكبر الصوت الخارجي. + var isSpeakerOn = false.obs; + + // EN: Timer countdown variable, starts from 60 seconds. + // AR: متغير العد التنازلي للمؤقت، يبدأ من 60 ثانية. + var elapsedSeconds = 60.obs; + + // --- Core State Variables / متغيرات الحالة الأساسية --- + + // EN: Flag to determine if the current user initiated the call. + // AR: مؤشر لتحديد ما إذا كان المستخدم الحالي هو من بدأ المكالمة. + bool isCaller = false; + + // EN: ID of the current user. + // AR: معرف المستخدم الحالي. + String currentUserId = ""; + + // --- WebRTC Internal Variables / متغيرات WebRTC الداخلية --- + + // EN: The main connection object between peers. + // AR: كائن الاتصال الرئيسي بين الطرفين. + rtc.RTCPeerConnection? _peerConnection; + + // EN: The local audio stream captured from the microphone. + // AR: دفق الصوت المحلي الملتقط من الميكروفون. + rtc.MediaStream? _localStream; + + // EN: Timer to enforce the 60-second call limit. + // AR: مؤقت لفرض حد الـ 60 ثانية للمكالمة. + Timer? _countdownTimer; + + // EN: Timer to hang up if the call is not answered within 30 seconds. + // AR: مؤقت لإنهاء المكالمة إذا لم يتم الرد خلال 30 ثانية. + Timer? _ringingTimeoutTimer; + + // EN: Flag to indicate if the peer connection is currently attempting ICE reconnection. + // AR: مؤشر يوضح ما إذا كان الاتصال يحاول إعادة بناء مسارات الشبكة حالياً. + bool _isReconnecting = false; + Timer? _reconnectTimer; + List _dynamicIceServers = []; + + AudioPlayer? _ringtonePlayer; + + void _startRingtone() async { + try { + _ringtonePlayer ??= AudioPlayer(); + await _ringtonePlayer!.setAsset('assets/start.wav'); + await _ringtonePlayer!.setLoopMode(LoopMode.one); + _ringtonePlayer!.play(); + } catch (e) { + Log.print("Error playing ringtone: $e"); + } + } + + void _stopRingtone() { + try { + _ringtonePlayer?.stop(); + } catch (e) { + Log.print("Error stopping ringtone: $e"); + } + } + + @override + void onInit() { + super.onInit(); + // EN: Add lifecycle observer. + // AR: إضافة مراقب لدورة حياة التطبيق. + WidgetsBinding.instance.addObserver(this); + + // EN: Initialize WebSocket signaling listeners. + // AR: تهيئة مستمعي إشارات الـ WebSocket. + _initSignalingCallbacks(); + } + + // EN: Lifecycle hook: handle app switching background/foreground. + // AR: معالجة انتقال التطبيق إلى الخلفية أو العودة للواجهة. + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + Log.print("VoiceCall: didChangeAppLifecycleState -> $state"); + if (state == AppLifecycleState.paused) { + Log.print( + "WARNING: App is in background. Microphone access might be suspended by the OS.", + ); + } else if (state == AppLifecycleState.resumed) { + Log.print("App resumed. Verifying WebRTC connection health."); + if (this.state.value == VoiceCallState.active) { + _ensureMicrophoneActive(); + _attemptIceRestart(); + } + } + } + + // EN: Registers all event listeners for the signaling server. + // AR: تسجيل جميع مستمعي الأحداث لخادم الإشارات. + void _initSignalingCallbacks() { + // EN: Triggered when successfully connected to the signaling server. + // AR: يُستدعى عند الاتصال بنجاح بخادم الإشارات. + _signaling.onConnected = (iceServers) { + Log.print("WebRTC Signaling Connected & Authenticated"); + _dynamicIceServers = iceServers; + }; + + // EN: Triggered when the WebSocket connection drops. + // AR: يُستدعى عند انقطاع اتصال الـ WebSocket. + _signaling.onDisconnected = (reason) { + Log.print("WebRTC Signaling Disconnected: $reason"); + if (state.value != VoiceCallState.idle) { + _endCallInternal("signaling_disconnected"); + } + }; + + // EN: Triggered when the remote user joins the room. + // AR: يُستدعى عند انضمام الطرف الآخر إلى غرفة الاتصال. + _signaling.onParticipantJoined = () async { + Log.print("Remote participant joined signaling session"); + // EN: If we are the caller, initiate the WebRTC handshake by creating an Offer. + // AR: إذا كنا نحن المتصل، نبدأ مصافحة WebRTC بإنشاء عرض (Offer). + if (isCaller && state.value == VoiceCallState.dialing) { + state.value = VoiceCallState.connecting; + await _createOffer(); + } + }; + + // EN: Triggered when an SDP Offer is received from the remote peer. + // AR: يُستدعى عند استلام عرض اتصال (Offer) من الطرف الآخر. + _signaling.onOffer = (sdpMap) async { + Log.print("Received WebRTC SDP Offer"); + if (!isCaller) { + state.value = VoiceCallState.connecting; + await _initializePeerConnection(); + + // EN: Set the remote peer's settings. + // AR: تعيين إعدادات الطرف الآخر. + final description = rtc.RTCSessionDescription( + sdpMap['sdp'], + sdpMap['type'], + ); + await _peerConnection!.setRemoteDescription(description); + + // EN: Respond with an Answer. + // AR: الرد بإجابة (Answer). + await _createAnswer(); + } + }; + + // EN: Triggered when an SDP Answer is received. + // AR: يُستدعى عند استلام إجابة (Answer) من الطرف الآخر. + _signaling.onAnswer = (sdpMap) async { + Log.print("Received WebRTC SDP Answer"); + if (isCaller && _peerConnection != null) { + final description = rtc.RTCSessionDescription( + sdpMap['sdp'], + sdpMap['type'], + ); + await _peerConnection!.setRemoteDescription(description); + } + }; + + // EN: Triggered when ICE candidates (Network routing info) are exchanged. + // AR: يُستدعى عند تبادل مسارات الشبكة (ICE Candidates) لتأسيس الاتصال. + _signaling.onIceCandidate = (candidateMap) async { + Log.print("Received Remote ICE Candidate"); + if (_peerConnection != null) { + final candidate = rtc.RTCIceCandidate( + candidateMap['candidate'], + candidateMap['sdpMid'], + candidateMap['sdpMLineIndex'], + ); + await _peerConnection!.addCandidate(candidate); + } + }; + + // EN: Triggered when a hangup event is received from the server. + // AR: يُستدعى عند استلام حدث إنهاء المكالمة من السيرفر. + _signaling.onCallEnded = (reason) { + Log.print("WebRTC Call Ended: $reason"); + _endCallInternal(reason); + }; + } + + // --- CALL LIFECYCLE / دورة حياة المكالمة --- + + // EN: Initiates an outgoing call. + // AR: يبدأ مكالمة صادرة. + Future startCall({ + required String rideIdVal, + required String driverId, + required String passengerId, + required String remoteNameVal, + }) async { + if (state.value != VoiceCallState.idle) return; + + // EN: Setup call variables. + // AR: إعداد متغيرات المكالمة. + state.value = VoiceCallState.dialing; + isCaller = true; + currentUserId = passengerId; + rideId.value = rideIdVal; + remoteName.value = remoteNameVal; + isMuted.value = false; + isSpeakerOn.value = false; + elapsedSeconds.value = 60; + _isReconnecting = false; + + _showCallBottomSheet(); + HapticFeedback.vibrate(); + + try { + // 1. EN: Request Microphone Permission / AR: طلب صلاحية الميكروفون + final permissionStatus = await Permission.microphone.request(); + if (!permissionStatus.isGranted) { + _endCallInternal("permission_denied"); + Get.snackbar( + "Error", + "Microphone permission is required for voice calls".tr, + ); + return; + } + + // 2. EN: Call PHP Backend to create Node.js session & notify Driver via FCM. + // AR: استدعاء واجهة PHP لإنشاء الجلسة على Node.js وإشعار السائق عبر FCM. + final response = await CRUD().post( + link: "${AppLink.server}/ride/call/passenger/create_call_session.php", + payload: {'ride_id': rideIdVal}, + ); + + if (response == null || + response == 'failure' || + response['status'] != 'success') { + _endCallInternal("session_creation_failed"); + Get.snackbar( + "Error", + "Failed to initiate call session. Please try again.".tr, + ); + return; + } + + final data = response['data']; + sessionId.value = data['session_id']; + + // 3. EN: Connect to WebRTC signaling server / AR: الاتصال بخادم الإشارات + await _signaling.connect(sessionId.value, currentUserId); + + // 4. EN: Initialize Local WebRTC Audio Stream / AR: تهيئة دفق الصوت المحلي + await _initializeLocalStream(); + + // 5. EN: Start Ringing Timeout Timer (30s max wait for driver to answer). + // AR: بدء مؤقت الرنين (أقصى انتظار 30 ثانية لرد السائق). + _ringingTimeoutTimer = Timer(const Duration(seconds: 30), () { + if (state.value == VoiceCallState.dialing) { + _signaling.send("hangup", {"reason": "no_answer"}); + _endCallInternal("no_answer"); + } + }); + } catch (e) { + Log.print("Error starting WebRTC call: $e"); + _endCallInternal("error"); + } + } + + // EN: Handles incoming call requests via FCM/Socket. + // AR: معالجة طلبات المكالمات الواردة. + Future receiveCall({ + required String sessionIdVal, + required String remoteNameVal, + required String rideIdVal, + }) async { + // EN: If already in a call, send busy signal. + // AR: إذا كان في مكالمة بالفعل، إرسال إشارة مشغول. + if (state.value != VoiceCallState.idle) { + _signaling.send("hangup", {"reason": "busy"}); + return; + } + + state.value = VoiceCallState.ringing; + isCaller = false; + currentUserId = box.read(BoxName.passengerID).toString(); + sessionId.value = sessionIdVal; + rideId.value = rideIdVal; + remoteName.value = remoteNameVal; + isMuted.value = false; + isSpeakerOn.value = false; + elapsedSeconds.value = 60; + _isReconnecting = false; + + _showCallBottomSheet(); + _startRingtone(); + HapticFeedback.vibrate(); + + // EN: Max 30s ringing timeout for receiver before auto-decline. + // AR: أقصى مدة للرنين 30 ثانية قبل الرفض التلقائي. + _ringingTimeoutTimer = Timer(const Duration(seconds: 30), () { + if (state.value == VoiceCallState.ringing) { + declineCall(); + } + }); + } + + // EN: Accepts the incoming call. + // AR: قبول المكالمة الواردة. + Future acceptCall() async { + if (state.value != VoiceCallState.ringing) return; + + _ringingTimeoutTimer?.cancel(); + _stopRingtone(); + state.value = VoiceCallState.connecting; + + try { + // EN: Check Mic permissions / AR: التحقق من صلاحيات الميكروفون + final permissionStatus = await Permission.microphone.request(); + if (!permissionStatus.isGranted) { + declineCall(); + Get.snackbar( + "Error", + "Microphone permission is required for voice calls".tr, + ); + return; + } + + await _signaling.connect(sessionId.value, currentUserId); + await _initializeLocalStream(); + + // EN: Notify caller we accepted / AR: إشعار المتصل بأننا قبلنا المكالمة + _signaling.send("join", {}); + } catch (e) { + Log.print("Error accepting call: $e"); + declineCall(); + } + } + + // EN: Declines an incoming call. + // AR: رفض المكالمة الواردة. + void declineCall() { + _ringingTimeoutTimer?.cancel(); + _stopRingtone(); + _signaling.send("hangup", {"reason": "declined"}); + _endCallInternal("declined"); + } + + // EN: Ends an active or dialing call. + // AR: إنهاء المكالمة النشطة أو الجاري الاتصال بها. + void hangup() { + _signaling.send("hangup", {"reason": "normal"}); + _endCallInternal("hangup"); + } + + // --- WEBRTC CORE HELPERS / دوال WebRTC الأساسية --- + + // EN: Captures the audio from the microphone with optimization constraints. + // AR: التقاط الصوت من الميكروفون مع قيود تحسين الجودة (إلغاء الصدى والضوضاء). + Future _initializeLocalStream() async { + final Map mediaConstraints = { + 'audio': { + 'echoCancellation': true, + 'noiseSuppression': true, + 'autoGainControl': true, + }, + 'video': false, // EN: Audio only / AR: صوت فقط + }; + + _localStream = await rtc.navigator.mediaDevices.getUserMedia( + mediaConstraints, + ); + rtc.Helper.setSpeakerphoneOn(isSpeakerOn.value); + } + + // EN: Verifies local microphone stream health on app resume and recreates/replaces track if suspended. + // AR: التحقق من سلامة مسار الميكروفون المحلي عند استئناف التطبيق وإعادة إنشائه إذا تم تعليقه. + Future _ensureMicrophoneActive() async { + if (_localStream == null || _peerConnection == null) return; + + bool needsRecreation = false; + if (_localStream!.active == false) { + needsRecreation = true; + } else { + for (var track in _localStream!.getAudioTracks()) { + if (!track.enabled && !isMuted.value) { + needsRecreation = true; + break; + } + } + } + + if (needsRecreation) { + Log.print( + "Local audio track ended or disabled. Recreating local stream...", + ); + try { + _localStream?.getTracks().forEach((track) => track.stop()); + _localStream?.dispose(); + _localStream = null; + + await _initializeLocalStream(); + + final senders = await _peerConnection!.getSenders(); + for (var sender in senders) { + final track = sender.track; + if (track != null && track.kind == 'audio') { + final newTracks = _localStream?.getAudioTracks(); + if (newTracks != null && newTracks.isNotEmpty) { + await sender.replaceTrack(newTracks.first); + Log.print( + "Replaced suspended/ended audio track with a new active one.", + ); + } + break; + } + } + } catch (e) { + Log.print("Error recreating local stream on resume: $e"); + } + } else { + _localStream!.getAudioTracks().forEach((track) { + track.enabled = !isMuted.value; + }); + } + } + + // EN: Creates the peer connection object and sets up ICE servers (STUN/TURN). + // AR: إنشاء كائن الاتصال المباشر وإعداد خوادم STUN/TURN لاختراق الجدران النارية. + Future _initializePeerConnection() async { + if (_peerConnection != null) return; + + final List> iceServers = []; + if (_dynamicIceServers.isNotEmpty) { + for (var server in _dynamicIceServers) { + if (server is Map) { + iceServers.add({ + "urls": server["urls"] ?? server["url"], + if (server["username"] != null) "username": server["username"], + if (server["credential"] != null) + "credential": server["credential"], + }); + } + } + } else { + // EN: Fallback STUN servers / AR: خوادم STUN الاحتياطية + iceServers.addAll([ + {"urls": "stun:stun.l.google.com:19302"}, + {"urls": "stun:stun1.l.google.com:19302"}, + ]); + } + + final Map configuration = {"iceServers": iceServers}; + + _peerConnection = await rtc.createPeerConnection(configuration); + + // EN: Gather local network routing info and send to remote peer. + // AR: جمع بيانات مسارات الشبكة المحلية وإرسالها للطرف الآخر. + _peerConnection!.onIceCandidate = (candidate) { + if (candidate.candidate != null) { + _signaling.send("ice_candidate", { + "candidate": { + "candidate": candidate.candidate, + "sdpMid": candidate.sdpMid, + "sdpMLineIndex": candidate.sdpMLineIndex, + }, + }); + } + }; + + // EN: Monitor connection status changes and handle disconnections. + // AR: مراقبة تغيرات حالة الاتصال ومعالجة انقطاع الشبكة. + _peerConnection!.onConnectionState = (connState) { + Log.print("RTCPeerConnectionState: $connState"); + if (connState == + rtc.RTCPeerConnectionState.RTCPeerConnectionStateConnected) { + _onCallConnected(); + } else if (connState == + rtc.RTCPeerConnectionState.RTCPeerConnectionStateFailed || + connState == + rtc.RTCPeerConnectionState.RTCPeerConnectionStateDisconnected) { + _handleIceConnectionFailure(); + } + }; + + // EN: Add local audio stream to the connection to send it to the other peer. + // AR: إضافة دفق الصوت المحلي للاتصال لإرساله للطرف الآخر. + if (_localStream != null) { + _localStream!.getTracks().forEach((track) { + _peerConnection!.addTrack(track, _localStream!); + }); + } + } + + // EN: Attempts an ICE restart to reconnect the WebRTC session when disconnections occur. + // AR: محاولة إعادة تأسيس الاتصال (ICE Restart) في حالة انقطاع الشبكة. + void _handleIceConnectionFailure() { + if (_isReconnecting) return; + _isReconnecting = true; + Log.print( + "ICE connection dropped. Attempting ICE Restart reconnection for 5s...", + ); + + if (isCaller) { + _attemptIceRestart(); + } + + _reconnectTimer?.cancel(); + _reconnectTimer = Timer(const Duration(seconds: 5), () { + if (state.value == VoiceCallState.active && + _peerConnection?.connectionState != + rtc.RTCPeerConnectionState.RTCPeerConnectionStateConnected) { + Log.print("ICE reconnection timed out. Hanging up."); + _endCallInternal("connection_lost"); + } else { + _isReconnecting = false; + Log.print("ICE Reconnection succeeded!"); + } + }); + } + + // EN: Initiates ICE Restart SDP exchange. + // AR: بدء تبادل حزم SDP لإعادة بناء مسارات الاتصال. + Future _attemptIceRestart() async { + if (_peerConnection == null || !isCaller) return; + try { + Log.print("Caller initiating WebRTC ICE Restart..."); + final constraints = { + 'mandatory': { + 'OfferToReceiveAudio': true, + 'OfferToReceiveVideo': false, + }, + 'optional': [ + {'IceRestart': true}, + ], + }; + final offer = await _peerConnection!.createOffer(constraints); + await _peerConnection!.setLocalDescription(offer); + _signaling.send("offer", { + "sdp": {"sdp": offer.sdp, "type": offer.type}, + }); + } catch (e) { + Log.print("Error initiating WebRTC ICE Restart: $e"); + } + } + + // EN: Generates an SDP Offer to initialize the connection. + // AR: إنشاء عرض (Offer) لبدء الاتصال وتحديد قدرات الجهاز. + Future _createOffer() async { + await _initializePeerConnection(); + + final constraints = { + 'mandatory': {'OfferToReceiveAudio': true, 'OfferToReceiveVideo': false}, + 'optional': [], + }; + + final offer = await _peerConnection!.createOffer(constraints); + await _peerConnection!.setLocalDescription(offer); + + _signaling.send("offer", { + "sdp": {"sdp": offer.sdp, "type": offer.type}, + }); + } + + // EN: Generates an SDP Answer in response to an Offer. + // AR: الرد بإنشاء إجابة (Answer) بناءً على العرض المستلم. + Future _createAnswer() async { + final constraints = { + 'mandatory': {'OfferToReceiveAudio': true, 'OfferToReceiveVideo': false}, + 'optional': [], + }; + + final answer = await _peerConnection!.createAnswer(constraints); + await _peerConnection!.setLocalDescription(answer); + + _signaling.send("answer", { + "sdp": {"sdp": answer.sdp, "type": answer.type}, + }); + } + + // EN: Triggered when connection is fully established. Starts the 60s timer. + // AR: يُستدعى عند تأسيس الاتصال بنجاح، ويقوم ببدء مؤقت الـ 60 ثانية. + void _onCallConnected() { + _ringingTimeoutTimer?.cancel(); + _reconnectTimer?.cancel(); + _isReconnecting = false; + + if (state.value != VoiceCallState.active) { + state.value = VoiceCallState.active; + HapticFeedback.vibrate(); + + // EN: Start 120s countdown timer / AR: بدء العد التنازلي لمدة 120 ثانية + _countdownTimer?.cancel(); + elapsedSeconds.value = 120; + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (elapsedSeconds.value > 1) { + elapsedSeconds.value--; + } else { + elapsedSeconds.value = 0; + _countdownTimer?.cancel(); + // EN: Force hangup when timer reaches 0 / AR: إغلاق إجباري عند وصول المؤقت لصفر + hangup(); + } + }); + } + } + + // EN: Internal cleanup function. Closes all connections and streams. + // AR: دالة التنظيف الداخلية. تقوم بإغلاق جميع الاتصالات وتفريغ الذاكرة. + void _endCallInternal(String reason) { + _countdownTimer?.cancel(); + _ringingTimeoutTimer?.cancel(); + _reconnectTimer?.cancel(); + _stopRingtone(); + + state.value = VoiceCallState.ended; + + // EN: Close WebRTC connection / AR: إغلاق اتصال WebRTC + _peerConnection?.close(); + _peerConnection = null; + + // EN: Stop mic capture / AR: إيقاف التقاط الميكروفون + _localStream?.getTracks().forEach((track) => track.stop()); + _localStream?.dispose(); + _localStream = null; + + // EN: Disconnect WebSockets / AR: إغلاق اتصال الـ WebSockets + _signaling.disconnect(); + + // EN: Close UI BottomSheet after delay / AR: إغلاق واجهة المكالمة بعد فترة زمنية قصيرة + Future.delayed(const Duration(milliseconds: 1500), () { + if (state.value == VoiceCallState.ended) { + state.value = VoiceCallState.idle; + Get.back(); + } + }); + } + + // --- ACTIONS (UI Controls) / إجراءات الواجهة --- + + // EN: Toggles microphone mute state. + // AR: تبديل حالة كتم الميكروفون. + void toggleMute() { + isMuted.value = !isMuted.value; + _localStream?.getAudioTracks().forEach((track) { + track.enabled = !isMuted.value; + }); + } + + // EN: Toggles loudspeaker mode. + // AR: تبديل حالة مكبر الصوت الخارجي. + void toggleSpeaker() { + isSpeakerOn.value = !isSpeakerOn.value; + rtc.Helper.setSpeakerphoneOn(isSpeakerOn.value); + } + + // EN: Displays the call UI overlay. + // AR: إظهار نافذة المكالمة السفلية. + void _showCallBottomSheet() { + Get.bottomSheet( + const VoiceCallBottomSheet(), + isScrollControlled: true, + enableDrag: false, + isDismissible: false, + ); + } + + // EN: Lifecycle hook: clean up resources when controller is destroyed. + // AR: دورة الحياة: تفريغ الذاكرة وإغلاق الموارد عند تدمير المتحكم. + @override + void onClose() { + WidgetsBinding.instance.removeObserver(this); + _countdownTimer?.cancel(); + _ringingTimeoutTimer?.cancel(); + _reconnectTimer?.cancel(); + _stopRingtone(); + _ringtonePlayer?.dispose(); + _peerConnection?.close(); + _localStream?.dispose(); + _signaling.disconnect(); + super.onClose(); + } +} diff --git a/lib/homw_widget.dart b/lib/homw_widget.dart deleted file mode 100644 index a2b0a55..0000000 --- a/lib/homw_widget.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:Intaleq/print.dart'; -import 'package:flutter/services.dart'; - -class WidgetManager { - static const platform = MethodChannel('com.mobileapp.store.ride/widget'); - - static Future updateWidget() async { - try { - await platform.invokeMethod('updateWidget'); - } on PlatformException catch (e) { - Log.print("Failed to update widget: '${e.message}'."); - } - } -} - -// Example usage: -void updateHomeScreenWidget() { - WidgetManager.updateWidget(); -} \ No newline at end of file diff --git a/lib/services/signaling_service.dart b/lib/services/signaling_service.dart new file mode 100644 index 0000000..be9d5ed --- /dev/null +++ b/lib/services/signaling_service.dart @@ -0,0 +1,111 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:Intaleq/print.dart'; + +class SignalingService { + WebSocket? _socket; + final String _url = "wss://calls.intaleqapp.com/ws"; + + // Callbacks + Function(List iceServers)? onConnected; + Function(String reason)? onDisconnected; + Function(Map offer)? onOffer; + Function(Map answer)? onAnswer; + Function(Map candidate)? onIceCandidate; + Function(String reason)? onCallEnded; + Function()? onParticipantJoined; + + bool get isConnected => _socket != null && _socket!.readyState == WebSocket.open; + + Future connect(String sessionId, String userId) async { + if (isConnected) return; + + try { + Log.print("Signaling: Connecting to $_url"); + _socket = await WebSocket.connect(_url) + .timeout(const Duration(seconds: 8)); + + _socket!.listen( + (data) { + _handleMessage(data); + }, + onError: (err) { + Log.print("Signaling socket error: $err"); + disconnect("socket_error"); + }, + onDone: () { + Log.print("Signaling socket closed by server"); + disconnect("socket_closed"); + }, + cancelOnError: true, + ); + + // Send the authenticate message as the first message + send("authenticate", { + "session_id": sessionId, + "user_id": userId, + }); + } catch (e) { + Log.print("Signaling connection failed: $e"); + onDisconnected?.call("connection_failed"); + } + } + + void _handleMessage(dynamic data) { + try { + Log.print("Signaling received raw: $data"); + final message = jsonDecode(data); + if (message is! Map) return; + + final type = message['type']; + switch (type) { + case 'authenticated': + final iceServers = message['ice_servers'] as List? ?? []; + onConnected?.call(iceServers); + break; + case 'participant_joined': + onParticipantJoined?.call(); + break; + case 'offer': + if (message['sdp'] != null) { + onOffer?.call(message['sdp']); + } + break; + case 'answer': + if (message['sdp'] != null) { + onAnswer?.call(message['sdp']); + } + break; + case 'ice_candidate': + if (message['candidate'] != null) { + onIceCandidate?.call(message['candidate']); + } + break; + case 'call_ended': + onCallEnded?.call(message['reason'] ?? 'normal'); + break; + } + } catch (e) { + Log.print("Error handling signaling message: $e"); + } + } + + void send(String type, Map data) { + if (!isConnected) return; + final msg = jsonEncode({ + 'type': type, + ...data, + }); + Log.print("Signaling sending: $msg"); + _socket!.add(msg); + } + + void disconnect([String reason = "user_hangup"]) { + if (_socket != null) { + _socket!.close(); + _socket = null; + onDisconnected?.call(reason); + } + } +} diff --git a/lib/views/Rate/rate_captain.dart b/lib/views/Rate/rate_captain.dart index bb28033..19c5fd7 100644 --- a/lib/views/Rate/rate_captain.dart +++ b/lib/views/Rate/rate_captain.dart @@ -1,4 +1,4 @@ -import 'package:Intaleq/controller/home/map_passenger_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'package:get/get.dart'; @@ -40,7 +40,7 @@ class RateDriverFromPassenger extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( - '${'Total price to '.tr}${Get.find().driverName}', + '${'Total price to '.tr}${Get.find().driverName}', style: AppStyle.title, ), Row( @@ -189,7 +189,7 @@ class RateDriverFromPassenger extends StatelessWidget { enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: - AppColor.grayColor.withOpacity(0.5)), // Customize the border color + AppColor.grayColor.withValues(alpha: 0.5)), // Customize the border color ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( diff --git a/lib/views/auth/otp_page.dart b/lib/views/auth/otp_page.dart index 4d443b6..531079a 100644 --- a/lib/views/auth/otp_page.dart +++ b/lib/views/auth/otp_page.dart @@ -410,7 +410,7 @@ class _PhoneNumberScreenState extends State { final rawPhone = _phoneController.text.trim().replaceFirst('+', ''); final success = await PhoneAuthHelper.sendOtp(rawPhone); if (success && mounted) { - await PhoneAuthHelper.verifyOtp(rawPhone); + Get.to(() => OtpVerificationScreen(phoneNumber: rawPhone)); } if (mounted) setState(() => _isLoading = false); } @@ -537,7 +537,7 @@ class _OtpVerificationScreenState extends State { void _submit() async { if (_formKey.currentState!.validate()) { setState(() => _isLoading = true); - // Logic for OTP verification here + await PhoneAuthHelper.verifyOtp(widget.phoneNumber, _otpController.text.trim()); if (mounted) setState(() => _isLoading = false); } } @@ -596,7 +596,7 @@ class _OtpVerificationScreenState extends State { controller: _otpController, textAlign: TextAlign.center, keyboardType: TextInputType.number, - maxLength: 5, + maxLength: 3, style: TextStyle( fontSize: 30, fontWeight: FontWeight.w700, @@ -605,7 +605,7 @@ class _OtpVerificationScreenState extends State { ), decoration: InputDecoration( counterText: '', - hintText: '·····', + hintText: '···', hintStyle: TextStyle( color: isDark ? Colors.white12 : const Color(0xFFD1D5DB), letterSpacing: 20, @@ -615,7 +615,7 @@ class _OtpVerificationScreenState extends State { border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(vertical: 6), ), - validator: (v) => v == null || v.length < 5 ? '' : null, + validator: (v) => v == null || v.length < 3 ? '' : null, ), ), ), @@ -625,10 +625,10 @@ class _OtpVerificationScreenState extends State { ValueListenableBuilder( valueListenable: _otpController, builder: (_, value, __) { - final filled = value.text.length.clamp(0, 5); + final filled = value.text.length.clamp(0, 3); return Row( mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(5, (i) { + children: List.generate(3, (i) { final active = i < filled; return AnimatedContainer( duration: const Duration(milliseconds: 200), diff --git a/lib/views/auth/otp_token_page.dart b/lib/views/auth/otp_token_page.dart index f06997e..0b7921b 100644 --- a/lib/views/auth/otp_token_page.dart +++ b/lib/views/auth/otp_token_page.dart @@ -23,9 +23,9 @@ class OtpVerificationPage extends StatefulWidget { class _OtpVerificationPageState extends State { late final OtpVerificationController controller; - final List _focusNodes = List.generate(6, (index) => FocusNode()); + final List _focusNodes = List.generate(3, (index) => FocusNode()); final List _textControllers = - List.generate(5, (index) => TextEditingController()); + List.generate(3, (index) => TextEditingController()); @override void initState() { @@ -50,7 +50,7 @@ class _OtpVerificationPageState extends State { void _onOtpChanged(String value, int index) { if (value.isNotEmpty) { - if (index < 5) { + if (index < 2) { _focusNodes[index + 1].requestFocus(); } else { _focusNodes[index].unfocus(); // إلغاء التركيز بعد آخر حقل @@ -67,7 +67,7 @@ class _OtpVerificationPageState extends State { textDirection: TextDirection.ltr, // لضمان ترتيب الحقول من اليسار لليمين child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: List.generate(5, (index) { + children: List.generate(3, (index) { return SizedBox( width: 45, height: 55, diff --git a/lib/views/home/map_page_passenger.dart b/lib/views/home/map_page_passenger.dart index 11f55fd..6bbbb10 100644 --- a/lib/views/home/map_page_passenger.dart +++ b/lib/views/home/map_page_passenger.dart @@ -3,8 +3,13 @@ import 'package:get/get.dart'; import '../../constant/box_name.dart'; import '../../constant/colors.dart'; import '../../controller/functions/crud.dart'; -import '../../controller/functions/package_info.dart'; -import '../../controller/home/map_passenger_controller.dart'; +import '../../controller/home/map/map_socket_controller.dart'; +import '../../controller/home/map/map_engine_controller.dart'; +import '../../controller/home/map/location_search_controller.dart'; +import '../../controller/home/map/nearby_drivers_controller.dart'; +import '../../controller/home/map/ride_lifecycle_controller.dart'; +import '../../controller/home/map/ui_interactions_controller.dart'; +import '../../controller/home/map/ride_state.dart'; import '../../main.dart'; import '../../views/home/map_widget.dart/ride_begin_passenger.dart'; @@ -17,7 +22,7 @@ import 'map_widget.dart/google_map_passenger_widget.dart'; import 'map_widget.dart/left_main_menu_icons.dart'; import 'map_widget.dart/main_bottom_menu_map.dart'; import 'map_widget.dart/map_menu_widget.dart'; -import 'map_widget.dart/menu_map_page.dart'; +import '../../controller/functions/package_info.dart'; import 'map_widget.dart/passengerRideLoctionWidget.dart'; import 'map_widget.dart/payment_method.page.dart'; import 'map_widget.dart/points_page_for_rider.dart'; @@ -30,9 +35,14 @@ class MapPagePassenger extends StatelessWidget { @override Widget build(BuildContext context) { - Get.put(MapPassengerController()); - Get.put(MyMenuController()); - Get.put(CRUD()); + Get.find(); + Get.find(); + Get.find(); + Get.find(); + Get.find(); + Get.find(); + Get.find(); + Get.find(); WidgetsBinding.instance.addPostFrameCallback((_) { checkForUpdate(context); }); @@ -118,7 +128,7 @@ class CancelRidePageShow extends StatelessWidget { @override Widget build(BuildContext context) { - return GetBuilder( + return GetBuilder( builder: (controller) { // نستخدم RideState Enum لأنه أدق، أو نصلح المنطق النصي // الشرط: @@ -175,7 +185,7 @@ class PickerIconOnMap extends StatelessWidget { @override Widget build(BuildContext context) { - return GetBuilder( + return GetBuilder( builder: (controller) => controller.isPickerShown ? Positioned( bottom: Get.height * .2, diff --git a/lib/views/home/map_widget.dart/apply_order_widget.dart b/lib/views/home/map_widget.dart/apply_order_widget.dart index 82035fa..1905253 100644 --- a/lib/views/home/map_widget.dart/apply_order_widget.dart +++ b/lib/views/home/map_widget.dart/apply_order_widget.dart @@ -1,7 +1,9 @@ import 'package:Intaleq/constant/colors.dart'; import 'package:Intaleq/constant/links.dart'; import 'package:Intaleq/constant/style.dart'; -import 'package:Intaleq/controller/home/map_passenger_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_state.dart'; +import 'package:Intaleq/controller/voice_call_controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; @@ -10,6 +12,7 @@ import 'package:intl/intl.dart'; import '../../../constant/box_name.dart'; import '../../../controller/firebase/notification_service.dart'; import '../../../controller/functions/launch.dart'; +import '../../../controller/functions/crud.dart'; import '../../../main.dart'; class ApplyOrderWidget extends StatelessWidget { @@ -29,7 +32,7 @@ class ApplyOrderWidget extends StatelessWidget { } return Obx(() { - final controller = Get.find(); + final controller = Get.find(); final bool isVisible = controller.currentRideState.value == RideState.driverApplied || @@ -57,7 +60,7 @@ class ApplyOrderWidget extends StatelessWidget { ), // تغيير: تقليل الحواف الخارجية بشكل كبير padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - child: GetBuilder( + child: GetBuilder( builder: (c) { return Column( mainAxisSize: @@ -106,15 +109,18 @@ class ApplyOrderWidget extends StatelessWidget { // [NEW] 1. صف الرأس المضغوط (يحتوي الحالة + الإحصائيات + السعر) // --------------------------------------------------------------------------- Widget _buildCompactHeaderRow( - BuildContext context, MapPassengerController controller) { + BuildContext context, RideLifecycleController controller) { // تنسيق السعر final formatter = NumberFormat("#,###"); String formattedPrice = formatter.format(controller.totalPassenger); - // حساب الدقائق - int minutes = - (controller.timeToPassengerFromDriverAfterApplied / 60).ceil(); - if (minutes < 1) minutes = 1; + // حساب الدقائق من الوقت المتبقي الحي، وليس ETA الأصلي فقط. + final int secondsToPassenger = + controller.remainingTimeToPassengerFromDriverAfterApplied > 0 + ? controller.remainingTimeToPassengerFromDriverAfterApplied + : controller.timeToPassengerFromDriverAfterApplied; + final int minutes = + secondsToPassenger <= 0 ? 0 : (secondsToPassenger / 60).ceil(); // تنسيق المسافة String distanceDisplay = ""; @@ -151,7 +157,7 @@ class ApplyOrderWidget extends StatelessWidget { children: [ _buildMiniStatChip( icon: Icons.access_time_filled_rounded, - text: "$minutes ${'min'.tr}", + text: minutes > 0 ? "$minutes ${'min'.tr}" : "--", color: AppColor.primaryColor, bgColor: AppColor.primaryColor.withOpacity(0.1), ), @@ -229,7 +235,7 @@ class ApplyOrderWidget extends StatelessWidget { // [MODIFIED] 2. كرت المعلومات المضغوط جداً // --------------------------------------------------------------------------- Widget _buildCompactInfoCard(BuildContext context, - MapPassengerController controller, Color Function(String) parseColor) { + RideLifecycleController controller, Color Function(String) parseColor) { return Container( // تقليل الحواف الداخلية للكرت padding: const EdgeInsets.all(10), @@ -312,7 +318,7 @@ class ApplyOrderWidget extends StatelessWidget { } Widget _buildMicroCarIcon( - MapPassengerController controller, Color Function(String) parseColor) { + RideLifecycleController controller, Color Function(String) parseColor) { Color carColor = parseColor(controller.colorHex); return Container( height: 40, // تصغير من 50 @@ -343,9 +349,8 @@ class ApplyOrderWidget extends StatelessWidget { color: Get.isDarkMode ? Colors.grey[850] : const Color(0xFFF5F5F5), borderRadius: BorderRadius.circular(6), border: Border.all( - color: Get.isDarkMode - ? Colors.white10 - : Colors.grey.withOpacity(0.3)), + color: + Get.isDarkMode ? Colors.white10 : Colors.grey.withOpacity(0.3)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -374,7 +379,7 @@ class ApplyOrderWidget extends StatelessWidget { // [MODIFIED] 3. أزرار الاتصال (Slim Buttons) // --------------------------------------------------------------------------- Widget _buildCompactButtonsRow( - BuildContext context, MapPassengerController controller) { + BuildContext context, RideLifecycleController controller) { return SizedBox( height: 40, // تحديد ارتفاع ثابت وصغير للأزرار child: Row( @@ -397,7 +402,7 @@ class ApplyOrderWidget extends StatelessWidget { bgColor: AppColor.greenColor, onTap: () { HapticFeedback.heavyImpact(); - makePhoneCall(controller.driverPhone); + _showCallSelectionDialog(context, controller); }, isPrimary: true, ), @@ -407,6 +412,73 @@ class ApplyOrderWidget extends StatelessWidget { ); } + void _showCallSelectionDialog( + BuildContext context, RideLifecycleController controller) { + Get.dialog( + Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Call Options'.tr, + style: AppStyle.title + .copyWith(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + Text( + 'Choose how you want to call the driver'.tr, + style: const TextStyle(color: Colors.grey, fontSize: 14), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + ListTile( + leading: CircleAvatar( + backgroundColor: AppColor.greenColor.withOpacity(0.1), + child: Icon(Icons.phone_android_rounded, + color: AppColor.greenColor), + ), + title: Text('Standard Call'.tr, + style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: Text('Uses cellular network'.tr, + style: const TextStyle(fontSize: 12)), + onTap: () { + Get.back(); + makePhoneCall(controller.driverPhone); + }, + ), + const Divider(), + ListTile( + leading: CircleAvatar( + backgroundColor: AppColor.primaryColor.withOpacity(0.1), + child: Icon(Icons.wifi_calling_3_rounded, + color: AppColor.primaryColor), + ), + title: Text('Free Call'.tr, + style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: Text('Voice call over internet'.tr, + style: const TextStyle(fontSize: 12)), + onTap: () { + Get.back(); + final voiceCtrl = Get.find(); + final passengerId = box.read(BoxName.passengerID).toString(); + voiceCtrl.startCall( + rideIdVal: controller.rideId, + driverId: controller.driverId, + passengerId: passengerId, + remoteNameVal: controller.driverName, + ); + }, + ), + ], + ), + ), + ), + ); + } + Widget _buildSlimButton({ required String label, required IconData icon, @@ -444,7 +516,7 @@ class ApplyOrderWidget extends StatelessWidget { // --- النوافذ المنبثقة للرسائل (نفس الكود السابق مع تحسين بسيط) --- void _showContactOptionsDialog( - BuildContext context, MapPassengerController controller) { + BuildContext context, RideLifecycleController controller) { Get.bottomSheet( Container( padding: const EdgeInsets.all(20), @@ -470,7 +542,7 @@ class ApplyOrderWidget extends StatelessWidget { ); } - List _buildPredefinedMessages(MapPassengerController controller) { + List _buildPredefinedMessages(RideLifecycleController controller) { const messages = [ 'Hello, I\'m at the agreed-upon location', 'I\'m waiting for you', @@ -510,7 +582,7 @@ class ApplyOrderWidget extends StatelessWidget { } Widget _buildCustomMessageInput( - MapPassengerController controller, BuildContext context) { + RideLifecycleController controller, BuildContext context) { return Row( children: [ Expanded( @@ -555,7 +627,22 @@ class ApplyOrderWidget extends StatelessWidget { ); } - void _sendMessage(MapPassengerController controller, String text) { + void _sendMessage(RideLifecycleController controller, String text) async { + try { + await CRUD().post( + link: AppLink.sendChatMessage, + payload: { + 'ride_id': controller.rideId.toString(), + 'sender_id': box.read(BoxName.passengerID).toString(), + 'receiver_id': controller.driverId.toString(), + 'sender_type': 'passenger', + 'message_content': text.tr, + }, + ); + } catch (e) { + // Ignore or log error + } + NotificationService.sendNotification( category: 'MSG_FROM_PASSENGER', target: controller.driverToken.toString(), @@ -577,7 +664,7 @@ class DriverArrivePassengerAndWaitMinute extends StatelessWidget { @override Widget build(BuildContext context) { - return GetBuilder(builder: (controller) { + return GetBuilder(builder: (controller) { return Column( children: [ Row( @@ -619,7 +706,7 @@ class TimeDriverToPassenger extends StatelessWidget { @override Widget build(BuildContext context) { - return GetBuilder(builder: (controller) { + return GetBuilder(builder: (controller) { if (controller.timeToPassengerFromDriverAfterApplied <= 0) { return const SizedBox(); } diff --git a/lib/views/home/map_widget.dart/buttom_sheet_map_show.dart b/lib/views/home/map_widget.dart/buttom_sheet_map_show.dart index f51c7c4..2e6c83a 100644 --- a/lib/views/home/map_widget.dart/buttom_sheet_map_show.dart +++ b/lib/views/home/map_widget.dart/buttom_sheet_map_show.dart @@ -3,11 +3,11 @@ import 'package:get/get.dart'; import 'package:Intaleq/controller/payment/payment_controller.dart'; import '../../../constant/style.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; -GetBuilder buttomSheetMapPage() { +GetBuilder buttomSheetMapPage() { Get.put(PaymentController()); - return GetBuilder( + return GetBuilder( builder: (controller) => controller.isBottomSheetShown && controller.rideConfirm == false ? const Positioned( @@ -508,7 +508,7 @@ class Details extends StatelessWidget { @override Widget build(BuildContext context) { - return GetBuilder( + return GetBuilder( builder: (controller) => Column( children: [ Row( diff --git a/lib/views/home/map_widget.dart/cancel_raide_page.dart b/lib/views/home/map_widget.dart/cancel_raide_page.dart index 41d4b67..1ceb4c3 100644 --- a/lib/views/home/map_widget.dart/cancel_raide_page.dart +++ b/lib/views/home/map_widget.dart/cancel_raide_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:Intaleq/constant/colors.dart'; import 'package:Intaleq/constant/style.dart'; -import 'package:Intaleq/controller/home/map_passenger_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart'; import '../../widgets/elevated_btn.dart'; // دالة لإظهار الشيت @@ -21,7 +21,7 @@ class CancelRidePageWidget extends StatelessWidget { @override Widget build(BuildContext context) { // تأكد من وجود الكنترولر - final controller = Get.find(); + final controller = Get.find(); final List reasons = [ "Changed my mind".tr, @@ -39,7 +39,7 @@ class CancelRidePageWidget extends StatelessWidget { color: AppColor.secondaryColor, borderRadius: const BorderRadius.vertical(top: Radius.circular(25)), ), - child: GetBuilder( + child: GetBuilder( builder: (controller) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ diff --git a/lib/views/home/map_widget.dart/car_details_widget_to_go.dart b/lib/views/home/map_widget.dart/car_details_widget_to_go.dart index 29bb104..ea36f4d 100644 --- a/lib/views/home/map_widget.dart/car_details_widget_to_go.dart +++ b/lib/views/home/map_widget.dart/car_details_widget_to_go.dart @@ -12,7 +12,7 @@ import 'dart:ui'; import '../../../constant/info.dart'; import '../../../controller/functions/tts.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; import '../../widgets/mydialoug.dart'; // ───────────────────────────────────────────────────────────────────────────── @@ -62,7 +62,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { CarDetailsTypeToChoose({super.key}); final textToSpeechController = Get.find(); - void _prepareCarTypes(MapPassengerController controller) { + void _prepareCarTypes(RideLifecycleController controller) { if (controller.distance > 23) { if (!carTypes.any((car) => car.carType == 'Rayeh Gai')) { carTypes.add(CarType( @@ -77,7 +77,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { @override Widget build(BuildContext context) { - return GetBuilder(builder: (controller) { + return GetBuilder(builder: (controller) { _prepareCarTypes(controller); if (!(controller.isBottomSheetShown) && controller.rideConfirm == false) { @@ -170,7 +170,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { // ═══════════════════════════════════════════════════════════════════════════ // HEADER // ═══════════════════════════════════════════════════════════════════════════ - Widget _buildHeader(MapPassengerController controller) { + Widget _buildHeader(RideLifecycleController controller) { return Padding( padding: const EdgeInsets.fromLTRB(22, 4, 22, 8), child: Column( @@ -290,7 +290,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { // ═══════════════════════════════════════════════════════════════════════════ // CAR CARD // ═══════════════════════════════════════════════════════════════════════════ - Widget _buildCarCard(BuildContext context, MapPassengerController controller, + Widget _buildCarCard(BuildContext context, RideLifecycleController controller, CarType carType, bool isSelected, int index) { return GestureDetector( onTap: () { @@ -437,7 +437,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { // PROMO BUTTON // ═══════════════════════════════════════════════════════════════════════════ Widget _buildPromoButton( - BuildContext context, MapPassengerController controller) { + BuildContext context, RideLifecycleController controller) { if (controller.promoTaken) return const SizedBox.shrink(); return Padding( @@ -511,7 +511,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { // ═══════════════════════════════════════════════════════════════════════════ // NEGATIVE BALANCE WARNING // ═══════════════════════════════════════════════════════════════════════════ - Widget _buildNegativeBalanceWarning(MapPassengerController controller) { + Widget _buildNegativeBalanceWarning(RideLifecycleController controller) { final passengerWallet = double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ?? 0.0; if (passengerWallet < 0.0) { @@ -556,7 +556,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { // PRICING HELPERS (Unchanged logic) // ═══════════════════════════════════════════════════════════════════════════ String _getPassengerPriceText( - CarType carType, MapPassengerController mapPassengerController) { + CarType carType, RideLifecycleController mapPassengerController) { double rawPrice; switch (carType.carType) { case 'Comfort': @@ -596,7 +596,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { // DIALOGS // ═══════════════════════════════════════════════════════════════════════════ void _showPromoCodeDialog( - BuildContext context, MapPassengerController controller) { + BuildContext context, RideLifecycleController controller) { Get.dialog( Dialog( shape: @@ -671,7 +671,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { void _showCarDetailsDialog( BuildContext context, - MapPassengerController mapPassengerController, + RideLifecycleController mapPassengerController, CarType carType, TextToSpeechController textToSpeechController) { Get.dialog( @@ -843,7 +843,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { // LOGIC HELPERS (Unchanged) // ═══════════════════════════════════════════════════════════════════════════ String _getCarDescription( - MapPassengerController mapPassengerController, CarType carType) { + RideLifecycleController mapPassengerController, CarType carType) { switch (carType.carType) { case 'Comfort': return mapPassengerController.endNameAddress @@ -881,7 +881,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { } void _handleCarSelection(BuildContext context, - MapPassengerController mapPassengerController, CarType carType) { + RideLifecycleController mapPassengerController, CarType carType) { box.write(BoxName.carType, carType.carType); mapPassengerController.totalPassenger = _getOriginalPrice(carType, mapPassengerController); @@ -932,7 +932,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { } double _getOriginalPrice( - CarType carType, MapPassengerController mapPassengerController) { + CarType carType, RideLifecycleController mapPassengerController) { switch (carType.carType) { case 'Comfort': return mapPassengerController.totalPassengerComfort; @@ -953,7 +953,7 @@ class CarDetailsTypeToChoose extends StatelessWidget { Widget _buildRayehGaiOption( BuildContext context, - MapPassengerController mapPassengerController, + RideLifecycleController mapPassengerController, String carTypeName, double price) { return GestureDetector( @@ -983,7 +983,7 @@ class BurcMoney extends StatelessWidget { @override Widget build(BuildContext context) { - return GetBuilder( + return GetBuilder( builder: (mapPassengerController) { final passengerWallet = double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ?? diff --git a/lib/views/home/map_widget.dart/cash_confirm_bottom_page.dart b/lib/views/home/map_widget.dart/cash_confirm_bottom_page.dart index 0937d4b..dbf072d 100644 --- a/lib/views/home/map_widget.dart/cash_confirm_bottom_page.dart +++ b/lib/views/home/map_widget.dart/cash_confirm_bottom_page.dart @@ -6,7 +6,7 @@ import 'package:Intaleq/views/home/my_wallet/passenger_wallet.dart'; import '../../../constant/colors.dart'; import '../../../constant/info.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; import '../../../controller/payment/payment_controller.dart'; import '../../../main.dart'; import '../../widgets/elevated_btn.dart'; @@ -17,7 +17,7 @@ class CashConfirmPageShown extends StatelessWidget { @override Widget build(BuildContext context) { - return GetBuilder(builder: (controller) { + return GetBuilder(builder: (controller) { // شرط الإظهار الرئيسي لم يتغير return Positioned( bottom: 0, diff --git a/lib/views/home/map_widget.dart/driver_card_from_passenger.dart b/lib/views/home/map_widget.dart/driver_card_from_passenger.dart index 913bbe3..48c0dd3 100644 --- a/lib/views/home/map_widget.dart/driver_card_from_passenger.dart +++ b/lib/views/home/map_widget.dart/driver_card_from_passenger.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../../constant/style.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; import 'hexegone_clipper.dart'; -GetBuilder hexagonClipper() { - return GetBuilder( +GetBuilder hexagonClipper() { + return GetBuilder( builder: ((controller) => controller.rideConfirm ? Positioned( top: Get.height * .1, diff --git a/lib/views/home/map_widget.dart/driver_time_arrive_passenger.dart b/lib/views/home/map_widget.dart/driver_time_arrive_passenger.dart index 680f899..7f24f65 100644 --- a/lib/views/home/map_widget.dart/driver_time_arrive_passenger.dart +++ b/lib/views/home/map_widget.dart/driver_time_arrive_passenger.dart @@ -4,14 +4,14 @@ import 'package:intl/intl.dart'; // import 'package:intl/intl.dart'; import '../../../constant/style.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; class DriverTimeArrivePassengerPage extends StatelessWidget { const DriverTimeArrivePassengerPage({super.key}); @override Widget build(BuildContext context) { - return GetBuilder( + return GetBuilder( builder: (controller) { return controller.remainingTime == 0 ? Positioned( diff --git a/lib/views/home/map_widget.dart/form_search_places_destenation.dart b/lib/views/home/map_widget.dart/form_search_places_destenation.dart index d424805..9725229 100644 --- a/lib/views/home/map_widget.dart/form_search_places_destenation.dart +++ b/lib/views/home/map_widget.dart/form_search_places_destenation.dart @@ -12,54 +12,51 @@ import 'package:Intaleq/views/widgets/elevated_btn.dart'; import '../../../constant/colors.dart'; import '../../../constant/style.dart'; import '../../../controller/functions/toast.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/location_search_controller.dart'; +import '../../../controller/home/map/map_engine_controller.dart'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; +import '../../../controller/home/map/ride_state.dart'; import '../../../main.dart'; // --------------------------------------------------- // -- Widget for Destination Point Search (Optimized) -- // --------------------------------------------------- -/// A more optimized and cleaner implementation of the destination search form. -/// -/// Improvements: -/// 1. **Widget Refactoring**: The UI is broken down into smaller, focused widgets -/// (_SearchField, _QuickActions, _SearchResults) to prevent unnecessary rebuilds. -/// 2. **State Management Scoping**: `GetBuilder` is used only on widgets that -/// actually need to update, not the entire form. -/// 3. **Reduced Build Logic**: Logic like reading from `box` is done once. -/// 4. **Readability**: Code is cleaner and easier to follow. -GetBuilder formSearchPlacesDestenation() { - // --- [تحسين] قراءة القيم مرة واحدة في بداية البناء --- - // Store box values in local variables to avoid repeated calls inside the build method. +GetBuilder formSearchPlacesDestenation() { final String addWorkValue = box.read(BoxName.addWork)?.toString() ?? 'addWork'; final String addHomeValue = box.read(BoxName.addHome)?.toString() ?? 'addHome'; - // --- [ملاحظة] تأكد من أن القيم الأولية موجودة --- - // This initialization can be moved to your app's startup logic or a splash screen controller. if (addWorkValue.isEmpty || addHomeValue.isEmpty) { box.write(BoxName.addWork, 'addWork'); box.write(BoxName.addHome, 'addHome'); } - return GetBuilder( - id: 'destination_form', // Use an ID to allow targeted updates + return GetBuilder( + id: 'destination_form', builder: (controller) { + final mapEngine = Get.find(); + final rideLifecycle = Get.find(); return Column( children: [ - // --- Widget for the search text field --- - _SearchField(controller: controller), - - // --- Widget for "Add Work" and "Add Home" buttons --- + _SearchField( + controller: controller, + mapEngine: mapEngine, + rideLifecycle: rideLifecycle, + ), _QuickActions( controller: controller, + mapEngine: mapEngine, + rideLifecycle: rideLifecycle, addWorkValue: addWorkValue, addHomeValue: addHomeValue, ), - - // --- Widget for displaying search results, wrapped in its own GetBuilder --- - _SearchResults(), + _SearchResults( + controller: controller, + mapEngine: mapEngine, + rideLifecycle: rideLifecycle, + ), ], ); }, @@ -70,11 +67,16 @@ GetBuilder formSearchPlacesDestenation() { // -- Private Helper Widgets for Cleaner Code -- // --------------------------------------------------- -/// A dedicated widget for the search input field. class _SearchField extends StatefulWidget { - final MapPassengerController controller; + final LocationSearchController controller; + final MapEngineController mapEngine; + final RideLifecycleController rideLifecycle; - const _SearchField({required this.controller}); + const _SearchField({ + required this.controller, + required this.mapEngine, + required this.rideLifecycle, + }); @override State<_SearchField> createState() => _SearchFieldState(); @@ -83,7 +85,6 @@ class _SearchField extends StatefulWidget { class _SearchFieldState extends State<_SearchField> { Timer? _debounce; - // --- [إصلاح] Listener لتحديث الواجهة عند تغيير النص لإظهار/إخفاء زر المسح --- void _onTextChanged() { if (mounted) { setState(() {}); @@ -93,20 +94,18 @@ class _SearchFieldState extends State<_SearchField> { @override void initState() { super.initState(); - // Add listener to update the suffix icon when text changes widget.controller.placeDestinationController.addListener(_onTextChanged); } - // --- [تحسين] إضافة Debouncer لتأخير البحث أثناء الكتابة --- void _onSearchChanged(String query) { if (_debounce?.isActive ?? false) _debounce!.cancel(); _debounce = Timer(const Duration(milliseconds: 500), () { if (query.length > 2) { widget.controller.getPlaces(); - widget.controller.changeHeightPlaces(); + widget.mapEngine.changeHeightPlaces(); } else if (query.isEmpty) { widget.controller.clearPlacesDestination(); - widget.controller.changeHeightPlaces(); + widget.mapEngine.changeHeightPlaces(); } }); } @@ -114,7 +113,6 @@ class _SearchFieldState extends State<_SearchField> { @override void dispose() { _debounce?.cancel(); - // Remove the listener to prevent memory leaks widget.controller.placeDestinationController.removeListener(_onTextChanged); super.dispose(); } @@ -133,18 +131,15 @@ class _SearchFieldState extends State<_SearchField> { hintText: widget.controller.hintTextDestinationPoint, hintStyle: AppStyle.subtitle.copyWith(color: Colors.grey[600]), prefixIcon: Icon(Icons.search, color: AppColor.primaryColor), - // --- [إصلاح] تم استبدال Obx بشرط بسيط لأن `setState` يعيد بناء الواجهة الآن --- suffixIcon: widget .controller.placeDestinationController.text.isNotEmpty ? IconButton( icon: Icon(Icons.clear, color: Colors.grey[400]), onPressed: () { widget.controller.placeDestinationController.clear(); - // The listener will automatically handle the UI update - // And _onSearchChanged will handle clearing the results }, ) - : null, // Use null instead of SizedBox for better practice + : null, contentPadding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 10.0), border: OutlineInputBorder( @@ -163,12 +158,12 @@ class _SearchFieldState extends State<_SearchField> { const SizedBox(width: 8.0), IconButton( onPressed: () { - widget.controller.changeMainBottomMenuMap(); - widget.controller.changePickerShown(); + widget.mapEngine.changeMainBottomMenuMap(); + widget.mapEngine.changePickerShown(); }, icon: Icon(Icons.location_on_outlined, color: AppColor.accentColor, size: 30), - tooltip: widget.controller.isAnotherOreder + tooltip: widget.rideLifecycle.isAnotherOreder ? 'Pick destination on map'.tr : 'Pick on map'.tr, ), @@ -178,14 +173,17 @@ class _SearchFieldState extends State<_SearchField> { } } -/// A dedicated widget for the quick action buttons (Work/Home). class _QuickActions extends StatelessWidget { - final MapPassengerController controller; + final LocationSearchController controller; + final MapEngineController mapEngine; + final RideLifecycleController rideLifecycle; final String addWorkValue; final String addHomeValue; const _QuickActions({ required this.controller, + required this.mapEngine, + required this.rideLifecycle, required this.addWorkValue, required this.addHomeValue, }); @@ -203,13 +201,20 @@ class _QuickActions extends StatelessWidget { onTap: () { if (addWorkValue == 'addWork') { controller.workLocationFromMap = true; - controller.changeMainBottomMenuMap(); - controller.changePickerShown(); + mapEngine.changeMainBottomMenuMap(); + mapEngine.changePickerShown(); } else { - _handleQuickAction(controller, BoxName.addWork, 'To Work'); + _handleQuickAction( + controller, + mapEngine, + rideLifecycle, + BoxName.addWork, + 'To Work', + ); } }, - onLongPress: () => _showChangeLocationDialog(controller, 'Work'), + onLongPress: () => + _showChangeLocationDialog(controller, mapEngine, 'Work'), ), _buildQuickActionButton( icon: Icons.home_outlined, @@ -217,13 +222,20 @@ class _QuickActions extends StatelessWidget { onTap: () { if (addHomeValue == 'addHome') { controller.homeLocationFromMap = true; - controller.changeMainBottomMenuMap(); - controller.changePickerShown(); + mapEngine.changeMainBottomMenuMap(); + mapEngine.changePickerShown(); } else { - _handleQuickAction(controller, BoxName.addHome, 'To Home'); + _handleQuickAction( + controller, + mapEngine, + rideLifecycle, + BoxName.addHome, + 'To Home', + ); } }, - onLongPress: () => _showChangeLocationDialog(controller, 'Home'), + onLongPress: () => + _showChangeLocationDialog(controller, mapEngine, 'Home'), ), ], ), @@ -231,17 +243,25 @@ class _QuickActions extends StatelessWidget { } } -/// A dedicated widget for the search results list. -/// It uses its own `GetBuilder` to only rebuild when the list of places changes. class _SearchResults extends StatelessWidget { + final LocationSearchController controller; + final MapEngineController mapEngine; + final RideLifecycleController rideLifecycle; + + const _SearchResults({ + required this.controller, + required this.mapEngine, + required this.rideLifecycle, + }); + @override Widget build(BuildContext context) { - return GetBuilder( - id: 'places_list', // Use a specific ID for targeted updates - builder: (controller) { + return GetBuilder( + id: 'places_list', + builder: (locCtrl) { return AnimatedContainer( duration: const Duration(milliseconds: 200), - height: controller.placesDestination.isNotEmpty ? 300 : 0, + height: locCtrl.placesDestination.isNotEmpty ? 300 : 0, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8.0), @@ -250,11 +270,11 @@ class _SearchResults extends StatelessWidget { child: ListView.separated( shrinkWrap: true, physics: const ClampingScrollPhysics(), - itemCount: controller.placesDestination.length, + itemCount: locCtrl.placesDestination.length, separatorBuilder: (context, index) => const Divider(height: 1, color: Colors.grey), itemBuilder: (BuildContext context, int index) { - final res = controller.placesDestination[index]; + final res = locCtrl.placesDestination[index]; final title = res['name_ar'] ?? res['name'] ?? 'Unknown Place'; final address = res['address'] ?? 'Details not available'; final latitude = res['latitude']; @@ -277,7 +297,14 @@ class _SearchResults extends StatelessWidget { context, latitude, longitude, title), ), onTap: () => _handlePlaceSelection( - controller, latitude, longitude, title, index), + controller, + mapEngine, + rideLifecycle, + latitude, + longitude, + title, + index, + ), ); }, ), @@ -286,7 +313,6 @@ class _SearchResults extends StatelessWidget { ); } - // --- [تحسين] استخراج المنطق المعقد إلى دوال مساعدة --- Future _handleAddToFavorites(BuildContext context, dynamic latitude, dynamic longitude, String title) async { if (latitude != null && longitude != null) { @@ -311,14 +337,19 @@ class _SearchResults extends StatelessWidget { } } - Future _handlePlaceSelection(MapPassengerController controller, - dynamic latitude, dynamic longitude, String title, int index) async { + Future _handlePlaceSelection( + LocationSearchController controller, + MapEngineController mapEngine, + RideLifecycleController rideLifecycle, + dynamic latitude, + dynamic longitude, + String title, + int index) async { if (latitude == null || longitude == null) { Toast.show(Get.context!, 'Invalid location data', AppColor.redColor); return; } - // Save to recent locations await sql.insertMapLocation({ 'latitude': latitude, 'longitude': longitude, @@ -330,53 +361,55 @@ class _SearchResults extends StatelessWidget { final destLatLng = LatLng( double.parse(latitude.toString()), double.parse(longitude.toString())); - if (controller.isAnotherOreder) { - // **Another Order Flow** - await _handleAnotherOrderSelection(controller, destLatLng); + if (rideLifecycle.isAnotherOreder) { + await _handleAnotherOrderSelection( + controller, mapEngine, rideLifecycle, destLatLng); } else { - // **Regular Order Flow** - _handleRegularOrderSelection(controller, destLatLng, index); + _handleRegularOrderSelection( + controller, mapEngine, rideLifecycle, destLatLng, index); } } Future _handleAnotherOrderSelection( - MapPassengerController controller, LatLng destination) async { + LocationSearchController controller, + MapEngineController mapEngine, + RideLifecycleController rideLifecycle, + LatLng destination) async { controller.myDestination = destination; - controller.clearPlacesDestination(); // Helper method in controller + controller.clearPlacesDestination(); - await controller.getDirectionMap( + await rideLifecycle.getDirectionMap( '${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}', '${controller.myDestination.latitude},${controller.myDestination.longitude}'); - controller.isPickerShown = false; + mapEngine.isPickerShown = false; controller.passengerStartLocationFromMap = false; - controller.changeMainBottomMenuMap(); - controller.showBottomSheet1(); + mapEngine.changeMainBottomMenuMap(); + rideLifecycle.showBottomSheet1(); } void _handleRegularOrderSelection( - MapPassengerController controller, LatLng destination, int index) { + LocationSearchController controller, + MapEngineController mapEngine, + RideLifecycleController rideLifecycle, + LatLng destination, + int index) { controller.passengerLocation = controller.newMyLocation; controller.myDestination = destination; controller.convertHintTextDestinationNewPlaces(index); - controller.clearPlacesDestination(); // Helper method in controller + controller.clearPlacesDestination(); - controller.changeMainBottomMenuMap(); + mapEngine.changeMainBottomMenuMap(); controller.passengerStartLocationFromMap = true; - controller.isPickerShown = true; + mapEngine.isPickerShown = true; - // ✅ FIX: Draw the route after setting destination (matching the "Another Order" flow) - controller.getDirectionMap( + rideLifecycle.getDirectionMap( '${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}', '${controller.myDestination.latitude},${controller.myDestination.longitude}'); } } -// --------------------------------------------------- -// -- Helper Functions (kept from original code) -- -// --------------------------------------------------- - Widget _buildQuickActionButton({ required IconData icon, required String text, @@ -410,10 +443,12 @@ Widget _buildQuickActionButton({ ); } -void _showChangeLocationDialog( - MapPassengerController controller, String locationType) { +void _showChangeLocationDialog(LocationSearchController controller, + MapEngineController mapEngine, String locationType) { MyDialog().getDialog( - locationType == 'Work' ? 'Change Work location ?'.tr : 'Change Home location ?'.tr, + locationType == 'Work' + ? 'Change Work location ?'.tr + : 'Change Home location ?'.tr, '', () { if (locationType == 'Work') { @@ -421,15 +456,18 @@ void _showChangeLocationDialog( } else { controller.homeLocationFromMap = true; } - controller.changeMainBottomMenuMap(); - controller.changePickerShown(); + mapEngine.changeMainBottomMenuMap(); + mapEngine.changePickerShown(); }, ); } void _handleQuickAction( - MapPassengerController controller, String boxName, String hintText) async { - // --- [تحسين] قراءة وتحويل الإحداثيات بأمان أكبر --- + LocationSearchController controller, + MapEngineController mapEngine, + RideLifecycleController rideLifecycle, + String boxName, + String hintText) async { try { final locationString = box.read(boxName).toString(); final parts = locationString.split(','); @@ -439,20 +477,19 @@ void _handleQuickAction( ); controller.hintTextDestinationPoint = hintText; - controller.changeMainBottomMenuMap(); + mapEngine.changeMainBottomMenuMap(); - await controller.getDirectionMap( + await rideLifecycle.getDirectionMap( '${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}', '${latLng.latitude},${latLng.longitude}', ); controller.currentLocationToFormPlaces = false; - controller.clearPlacesDestination(); // Helper method in controller + controller.clearPlacesDestination(); controller.passengerStartLocationFromMap = false; - controller.isPickerShown = false; - controller.showBottomSheet1(); + mapEngine.isPickerShown = false; + rideLifecycle.showBottomSheet1(); } catch (e) { - // Handle error if parsing fails Log.print("Error handling quick action: $e"); Toast.show(Get.context!, "Failed to get location".tr, AppColor.redColor); } diff --git a/lib/views/home/map_widget.dart/form_search_start.dart b/lib/views/home/map_widget.dart/form_search_start.dart index 91f8390..4fd7b05 100644 --- a/lib/views/home/map_widget.dart/form_search_start.dart +++ b/lib/views/home/map_widget.dart/form_search_start.dart @@ -4,135 +4,122 @@ import 'package:intaleq_maps/intaleq_maps.dart'; import '../../../constant/colors.dart'; import '../../../constant/style.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/location_search_controller.dart'; +import '../../../controller/home/map/map_engine_controller.dart'; // --------------------------------------------------- // -- Widget for Start Point Search (Updated) -- // --------------------------------------------------- -GetBuilder formSearchPlacesStart() { - return GetBuilder( - id: 'start_point_form', // إضافة معرف لتحديث هذا الجزء فقط عند الحاجة - builder: (controller) => Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - 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: - 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, +GetBuilder formSearchPlacesStart() { + return GetBuilder( + id: 'start_point_form', + builder: (controller) { + final mapEngine = Get.find(); + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + 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: + 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), - ), - filled: true, - fillColor: Colors.grey[50], ), ), - ), - - 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, - ), - ], + const SizedBox(width: 8.0), + IconButton( + onPressed: () { + controller.passengerStartLocationFromMap = true; + mapEngine.changeMainBottomMenuMap(); + mapEngine.changePickerShown(); + }, + icon: Icon(Icons.location_on_outlined, + color: AppColor.accentColor, size: 30), + tooltip: 'Pick start point on map'.tr, + ), + ], + ), ), - ), + AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: controller.placesStart.isNotEmpty ? 300 : 0, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.0), + ), + margin: const EdgeInsets.symmetric(horizontal: 16.0), + child: ListView.separated( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + itemCount: controller.placesStart.length, + separatorBuilder: (context, index) => + const Divider(height: 1, color: Colors.grey), + itemBuilder: (BuildContext context, int index) { + var res = controller.placesStart[index]; + var title = res['name_ar'] ?? res['name'] ?? 'Unknown Place'; + var address = res['address'] ?? 'Details not available'; - // --- قائمة نتائج البحث --- - AnimatedContainer( - duration: const Duration(milliseconds: 200), - height: controller.placesStart.isNotEmpty ? 300 : 0, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8.0), + return ListTile( + leading: const Icon(Icons.place, size: 30, color: Colors.grey), + title: Text(title, + style: AppStyle.subtitle + .copyWith(fontWeight: FontWeight.w500)), + subtitle: Text(address, + style: TextStyle(color: Colors.grey[600], fontSize: 12)), + onTap: () { + 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(); + mapEngine.changeMainBottomMenuMap(); + controller.update(); + } + }, + ); + }, + ), ), - margin: const EdgeInsets.symmetric(horizontal: 16.0), - child: ListView.separated( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - itemCount: controller.placesStart.length, - separatorBuilder: (context, index) => - const Divider(height: 1, color: Colors.grey), - itemBuilder: (BuildContext context, int index) { - var res = controller.placesStart[index]; - var title = res['name_ar'] ?? res['name'] ?? 'Unknown Place'; - var address = res['address'] ?? 'Details not available'; - - return ListTile( - leading: const Icon(Icons.place, size: 30, color: Colors.grey), - title: Text(title, - style: AppStyle.subtitle - .copyWith(fontWeight: FontWeight.w500)), - subtitle: Text(address, - style: TextStyle(color: Colors.grey[600], fontSize: 12)), - onTap: () { - 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(); - - // إغلاق القائمة والعودة للخريطة لرؤية النتيجة (اختياري حسب منطق تطبيقك) - controller.changeMainBottomMenuMap(); - - controller.update(); - } - }, - ); - }, - ), - ), - ], - ), + ], + ); + }, ); } diff --git a/lib/views/home/map_widget.dart/form_serch_multiy_point.dart b/lib/views/home/map_widget.dart/form_serch_multiy_point.dart index e0a5959..bb39930 100644 --- a/lib/views/home/map_widget.dart/form_serch_multiy_point.dart +++ b/lib/views/home/map_widget.dart/form_serch_multiy_point.dart @@ -6,183 +6,181 @@ import 'package:intaleq_maps/intaleq_maps.dart'; import '../../../constant/colors.dart'; import '../../../constant/style.dart'; import '../../../controller/functions/toast.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/location_search_controller.dart'; +import '../../../controller/home/map/map_engine_controller.dart'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; import '../../../main.dart'; -GetBuilder formSearchPlaces(int index) { - // DbSql sql = DbSql.instance; - return GetBuilder( - builder: (controller) => Column( - children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Container( - decoration: BoxDecoration(color: AppColor.secondaryColor), - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder( - borderRadius: BorderRadius.only(), - gapPadding: 4, - borderSide: BorderSide( - color: AppColor.redColor, - width: 2, - )), - suffixIcon: const Icon(Icons.search), - hintText: controller.hintTextwayPoint0.tr, - hintStyle: AppStyle.title, - hintMaxLines: 1, - prefixIcon: IconButton( - onPressed: () { - controller.allTextEditingPlaces[index].clear(); - controller.clearPlaces(index); - }, - icon: Icon( - Icons.clear, - color: Colors.red[300], - ), - ), - ), - controller: controller.allTextEditingPlaces[index], - onChanged: (value) { - if (controller.allTextEditingPlaces[index].text.length > - 5) { - controller.getPlacesListsWayPoint(index); - controller.changeHeightPlacesAll(index); - } +GetBuilder formSearchPlaces(int index) { + return GetBuilder( + builder: (controller) { + final mapEngine = Get.find(); + final rideLifecycle = Get.find(); + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Container( + decoration: BoxDecoration(color: AppColor.secondaryColor), + child: TextField( + decoration: InputDecoration( + border: const OutlineInputBorder( + borderRadius: BorderRadius.only(), + gapPadding: 4, + borderSide: BorderSide( + color: AppColor.redColor, + width: 2, + )), + suffixIcon: const Icon(Icons.search), + hintText: controller.hintTextwayPoint0.tr, + hintStyle: AppStyle.title, + hintMaxLines: 1, + prefixIcon: IconButton( + onPressed: () { + controller.allTextEditingPlaces[index].clear(); + controller.clearPlaces(index); }, - // onEditingComplete: () => controller.changeHeight(), + icon: Icon( + Icons.clear, + color: Colors.red[300], + ), ), ), + controller: controller.allTextEditingPlaces[index], + onChanged: (value) { + if (controller.allTextEditingPlaces[index].text.length > 5) { + controller.getPlacesListsWayPoint(index); + mapEngine.changeHeightPlacesAll(index); + } + }, ), - controller.placeListResponseAll[index].isEmpty - ? InkWell( - onTap: () { - controller.startLocationFromMapAll[index] = true; - controller.wayPointIndex = index; - Get.back(); - // controller.changeMainBottomMenuMap(); - controller.changeWayPointStopsSheet(); - controller.changePickerShown(); - }, - child: Text( - 'Choose from Map'.tr + ' $index'.tr, - style: - AppStyle.title.copyWith(color: AppColor.blueColor), - ), - ) - : const SizedBox(), - Container( - height: controller.placeListResponseAll[index].isNotEmpty - ? controller.height - : 0, - color: AppColor.secondaryColor, - child: ListView.builder( - itemCount: controller.placeListResponseAll[index].length, - itemBuilder: (BuildContext context, int i) { - var res = controller.placeListResponseAll[index][i]; - return InkWell( - onTap: () async { - // ── Extract selected location ── - final double lat = res['geometry']['location']['lat']; - final double lng = res['geometry']['location']['lng']; - final String placeName = res['name'].toString(); - final selectedLatLng = LatLng(lat, lng); + ), + ), + controller.placeListResponseAll[index].isEmpty + ? InkWell( + onTap: () { + controller.startLocationFromMapAll[index] = true; + controller.wayPointIndex = index; + Get.back(); + mapEngine.changeWayPointStopsSheet(); + mapEngine.changePickerShown(); + }, + child: Text( + 'Choose from Map'.tr + ' $index'.tr, + style: + AppStyle.title.copyWith(color: AppColor.blueColor), + ), + ) + : const SizedBox(), + Container( + height: controller.placeListResponseAll[index].isNotEmpty + ? mapEngine.height + : 0, + color: AppColor.secondaryColor, + child: ListView.builder( + itemCount: controller.placeListResponseAll[index].length, + itemBuilder: (BuildContext context, int i) { + var res = controller.placeListResponseAll[index][i]; + return InkWell( + onTap: () async { + final double lat = res['geometry']['location']['lat']; + final double lng = res['geometry']['location']['lng']; + final String placeName = res['name'].toString(); + final selectedLatLng = LatLng(lat, lng); - controller.changeHeightPlaces(); + mapEngine.changeHeightPlaces(); - // ── Update start/end based on context ── - if (controller.currentLocationToFormPlacesAll[index] == - true) { - controller.newStartPointLocation = - controller.passengerLocation; - } else { - controller.passengerLocation = - controller.newStartPointLocation; - } + if (controller.currentLocationToFormPlacesAll[index] == + true) { + controller.newStartPointLocation = + rideLifecycle.passengerLocation; + } else { + rideLifecycle.passengerLocation = + controller.newStartPointLocation; + } - // ✅ FIX: Set the waypoint to the selected location - controller.menuWaypoints[index] = selectedLatLng; - controller.menuWaypointNames[index] = placeName; + controller.menuWaypoints[index] = selectedLatLng; + controller.menuWaypointNames[index] = placeName; - // ✅ FIX: Update hint text and coordinates - controller.convertHintTextPlaces(index, res); + controller.convertHintTextPlaces(index, res); - // ✅ FIX: Draw the route with the updated waypoint - final String start = - '${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}'; - final String dest = - '${controller.myDestination.latitude},${controller.myDestination.longitude}'; + final String start = + '${rideLifecycle.passengerLocation.latitude},${rideLifecycle.passengerLocation.longitude}'; + final String dest = + '${rideLifecycle.myDestination.latitude},${rideLifecycle.myDestination.longitude}'; - await controller.getDirectionMap(start, dest); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Column( + await rideLifecycle.getDirectionMap(start, dest); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Column( children: [ - Column( - children: [ - Image.network( - res['icon'], - width: 20, - ), - IconButton( - onPressed: () async { - await sql.insertMapLocation({ - 'latitude': res['geometry'] - ['location']['lat'], - 'longitude': res['geometry'] - ['location']['lng'], - 'name': res['name'].toString(), - 'rate': res['rating'].toString(), - }, TableName.placesFavorite); - Toast.show( - context, - '${res['name']} ${'Saved Sucssefully'.tr}', - AppColor.primaryColor); - }, - icon: const Icon(Icons.favorite_border), - ), - ], + Image.network( + res['icon'], + width: 20, ), - Column( - children: [ - Text( - res['name'].toString(), - style: AppStyle.title, - ), - Text( - res['vicinity'].toString(), - style: AppStyle.subtitle, - ), - ], - ), - Column( - children: [ - Text( - 'rate', - style: AppStyle.subtitle, - ), - Text( - res['rating'].toString(), - style: AppStyle.subtitle, - ), - ], + IconButton( + onPressed: () async { + await sql.insertMapLocation({ + 'latitude': res['geometry'] + ['location']['lat'], + 'longitude': res['geometry'] + ['location']['lng'], + 'name': res['name'].toString(), + 'rate': res['rating'].toString(), + }, TableName.placesFavorite); + Toast.show( + context, + '${res['name']} ${'Saved Sucssefully'.tr}', + AppColor.primaryColor); + }, + icon: const Icon(Icons.favorite_border), + ), + ], + ), + Column( + children: [ + Text( + res['name'].toString(), + style: AppStyle.title, + ), + Text( + res['vicinity'].toString(), + style: AppStyle.subtitle, + ), + ], + ), + Column( + children: [ + Text( + 'rate', + style: AppStyle.subtitle, + ), + Text( + res['rating'].toString(), + style: AppStyle.subtitle, ), ], ), - const Divider( - thickness: 1, - ) ], ), - ), - ); - }, - ), - ) - ], - )); + const Divider( + thickness: 1, + ) + ], + ), + ), + ); + }, + ), + ) + ], + ); + }, + ); } diff --git a/lib/views/home/map_widget.dart/google_map_passenger_widget.dart b/lib/views/home/map_widget.dart/google_map_passenger_widget.dart index bace205..d5f526a 100644 --- a/lib/views/home/map_widget.dart/google_map_passenger_widget.dart +++ b/lib/views/home/map_widget.dart/google_map_passenger_widget.dart @@ -6,19 +6,26 @@ import 'package:intaleq_maps/intaleq_maps.dart'; import 'package:Intaleq/controller/home/points_for_rider_controller.dart'; import 'package:Intaleq/services/offline_map_service.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/location_search_controller.dart'; +import '../../../controller/home/map/map_engine_controller.dart'; +import '../../../controller/home/map/nearby_drivers_controller.dart'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; import '../../widgets/mycircular.dart'; import '../../widgets/mydialoug.dart'; class GoogleMapPassengerWidget extends StatelessWidget { GoogleMapPassengerWidget({super.key}); - final WayPointController wayPointController = Get.put(WayPointController()); + final WayPointController wayPointController = Get.find(); @override Widget build(BuildContext context) { - return GetBuilder( - builder: (controller) => controller.isLoading + final locationSearch = Get.find(); + final rideLifecycle = Get.find(); + final nearbyDrivers = Get.find(); + + return GetBuilder( + builder: (controller) => rideLifecycle.isLoading ? const MyCircularProgressIndicator() : Positioned( bottom: Get.height * .2, @@ -32,13 +39,13 @@ class GoogleMapPassengerWidget extends StatelessWidget { : 'assets/style.json', onMapCreated: controller.onMapCreated, onStyleLoaded: controller.onStyleLoaded, - onCameraMove: controller.onCameraMoveThrottled, + onCameraMove: locationSearch.onCameraMoveThrottled, onCameraIdle: () { if (controller.mapController != null) { final position = controller.mapController!.cameraPosition; if (position != null) { Log.print('✅ onCameraIdle targeted: ${position.target}'); - controller + locationSearch .updateCurrentLocationFromCamera(position.target); OfflineMapService.instance .downloadRegion(position.target, radiusKm: 1.0); @@ -54,8 +61,8 @@ class GoogleMapPassengerWidget extends StatelessWidget { polygons: controller.polygons, circles: controller.circles, initialCameraPosition: CameraPosition( - target: controller.passengerLocation, - zoom: controller.lowPerf ? 14.5 : 15, + target: locationSearch.passengerLocation, + zoom: nearbyDrivers.lowPerf ? 14.5 : 15, ), myLocationEnabled: true, onTap: (latlng) => controller.hidePlaces(), @@ -63,11 +70,11 @@ class GoogleMapPassengerWidget extends StatelessWidget { MyDialog().getDialog('Are you want to go to this site'.tr, '', () async { controller.clearPolyline(); - controller.getDirectionMap( - '${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}', + rideLifecycle.getDirectionMap( + '${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}', '${latlng.latitude},${latlng.longitude}', ); - controller.showBottomSheet1(); + rideLifecycle.showBottomSheet1(); }); }, ), diff --git a/lib/views/home/map_widget.dart/left_main_menu_icons.dart b/lib/views/home/map_widget.dart/left_main_menu_icons.dart index 14769d2..dd19460 100644 --- a/lib/views/home/map_widget.dart/left_main_menu_icons.dart +++ b/lib/views/home/map_widget.dart/left_main_menu_icons.dart @@ -1,95 +1,82 @@ import 'dart:math'; import 'package:Intaleq/views/widgets/elevated_btn.dart'; -import 'package:Intaleq/views/widgets/error_snakbar.dart'; import 'package:Intaleq/views/widgets/mycircular.dart'; import 'package:flutter/material.dart'; import 'package:flutter_font_icons/flutter_font_icons.dart'; import 'package:get/get.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; -import 'dart:ui'; // مهم لإضافة تأثير الضبابية +import 'dart:ui'; import '../../../constant/colors.dart'; -import '../../../controller/functions/tts.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/location_search_controller.dart'; +import '../../../controller/home/map/map_engine_controller.dart'; import '../../../controller/home/vip_waitting_page.dart'; import '../navigation/navigation_view.dart'; // --- الدالة الرئيسية بالتصميم الجديد --- -GetBuilder leftMainMenuIcons() { - return GetBuilder( - builder: (controller) => Positioned( - // تم تعديل الموضع ليتناسب مع التصميم الجديد - top: Get.height * .01, - left: 0, - right: 0, - child: Center( - child: ClipRRect( - borderRadius: BorderRadius.circular(50.0), // لإنشاء شكل الكبسولة - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 8.0, sigmaY: 8.0), // تأثير الزجاج المصنفر - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: AppColor.secondaryColor.withOpacity(0.4), // لون شبه شفاف - borderRadius: BorderRadius.circular(50.0), - border: Border.all(color: AppColor.secondaryColor), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, // ليأخذ الشريط حجم الأزرار فقط - children: [ - // --- تم استخدام دالة مساعدة جديدة للزر --- - _buildMapActionButton( - icon: Icons.near_me_outlined, - tooltip: 'Toggle Map Type', - onPressed: () => Get.to(() => NavigationView()), - ), - // _buildVerticalDivider(), - // _buildMapActionButton( - // icon: Icons.traffic_outlined, - // tooltip: 'Toggle Traffic', - // onPressed: () => controller.changeMapTraffic(), - // ), - _buildVerticalDivider(), - _buildMapActionButton( - icon: Icons.my_location_rounded, - tooltip: 'Go to My Location', - onPressed: () { - controller.mapController?.animateCamera( - CameraUpdate.newLatLng( - LatLng( - controller.passengerLocation.latitude, - controller.passengerLocation.longitude, +GetBuilder leftMainMenuIcons() { + return GetBuilder( + builder: (controller) { + final locationSearch = Get.find(); + return Positioned( + top: Get.height * .01, + left: 0, + right: 0, + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(50.0), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: AppColor.secondaryColor.withValues(alpha: 0.4), + borderRadius: BorderRadius.circular(50.0), + border: Border.all(color: AppColor.secondaryColor), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + _buildMapActionButton( + icon: Icons.near_me_outlined, + tooltip: 'Toggle Map Type', + onPressed: () => Get.to(() => NavigationView()), + ), + _buildVerticalDivider(), + _buildMapActionButton( + icon: Icons.my_location_rounded, + tooltip: 'Go to My Location', + onPressed: () { + controller.mapController?.animateCamera( + CameraUpdate.newLatLng( + LatLng( + locationSearch.passengerLocation.latitude, + locationSearch.passengerLocation.longitude, + ), ), - ), - ); - }, - ), - _buildVerticalDivider(), - _buildMapActionButton( - icon: Octicons.watch, - tooltip: 'VIP Waiting Page', - onPressed: () => Get.to(() => VipWaittingPage()), - ), - // _buildMapActionButton( - // icon: Octicons.ellipsis, - // tooltip: 'test', - // onPressed: () => Get.to(() => TestPage()), - // ), - ], + ); + }, + ), + _buildVerticalDivider(), + _buildMapActionButton( + icon: Octicons.watch, + tooltip: 'VIP Waiting Page', + onPressed: () => Get.to(() => VipWaittingPage()), + ), + ], + ), ), ), ), ), - ), - ), + ); + }, ); } -// --- دالة مساعدة جديدة لإنشاء الأزرار بشكل أنيق --- Widget _buildMapActionButton({ required IconData icon, required String tooltip, @@ -101,28 +88,23 @@ Widget _buildMapActionButton({ tooltip: tooltip, splashRadius: 22, padding: const EdgeInsets.all(12), - constraints: const BoxConstraints(), // لإزالة المساحات الافتراضية + constraints: const BoxConstraints(), ); } -// --- ويدجت للفاصل الرأسي بين الأزرار --- Widget _buildVerticalDivider() { return Container( height: 20, width: 1, - color: AppColor.writeColor.withOpacity(0.2), + color: AppColor.writeColor.withValues(alpha: 0.2), ); } -// --- باقي الكود الخاص بك يبقى كما هو بدون تغيير --- - class TestPage extends StatelessWidget { const TestPage({super.key}); @override Widget build(BuildContext context) { - final random = Random(); - return Scaffold( appBar: AppBar( title: const Text('iOS Live Activity Test'), @@ -137,7 +119,6 @@ class TestPage extends StatelessWidget { title: 'title', onPressed: () {}, ), - // زر الإنهاء ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.red, diff --git a/lib/views/home/map_widget.dart/main_bottom_Menu_map.dart b/lib/views/home/map_widget.dart/main_bottom_Menu_map.dart index edaca76..ac55085 100644 --- a/lib/views/home/map_widget.dart/main_bottom_Menu_map.dart +++ b/lib/views/home/map_widget.dart/main_bottom_Menu_map.dart @@ -5,7 +5,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:Intaleq/constant/box_name.dart'; import 'package:Intaleq/constant/style.dart'; -import 'package:Intaleq/controller/home/map_passenger_controller.dart'; +import 'package:Intaleq/controller/home/map/map_engine_controller.dart'; +import 'package:Intaleq/controller/home/map/location_search_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart'; import 'package:Intaleq/main.dart'; import 'package:Intaleq/views/home/map_widget.dart/form_search_places_destenation.dart'; import 'package:Intaleq/views/widgets/elevated_btn.dart'; @@ -21,19 +23,18 @@ import 'form_search_start.dart'; class _D { static const double radiusCard = 28; static const double radiusChip = 20; - static const double radiusBtn = 16; static const double radiusInner = 14; static const double radiusPill = 50; static List get cardShadow => [ BoxShadow( - color: Colors.black.withOpacity(0.08), + color: Colors.black.withValues(alpha: 0.08), blurRadius: 40, spreadRadius: -8, offset: const Offset(0, 12), ), BoxShadow( - color: Colors.black.withOpacity(0.04), + color: Colors.black.withValues(alpha: 0.04), blurRadius: 16, spreadRadius: -4, offset: const Offset(0, 4), @@ -42,13 +43,13 @@ class _D { static List glowShadow(Color c, {double intensity = 0.4}) => [ BoxShadow( - color: c.withOpacity(intensity), + color: c.withValues(alpha: intensity), blurRadius: 24, spreadRadius: -4, offset: const Offset(0, 8), ), BoxShadow( - color: c.withOpacity(intensity * 0.5), + color: c.withValues(alpha: intensity * 0.5), blurRadius: 12, spreadRadius: -2, offset: const Offset(0, 3), @@ -57,7 +58,6 @@ class _D { static const Duration fast = Duration(milliseconds: 180); static const Duration medium = Duration(milliseconds: 420); - static const Duration slow = Duration(milliseconds: 600); static LinearGradient primaryGradient({ Alignment begin = Alignment.topLeft, @@ -68,8 +68,8 @@ class _D { end: end, colors: [ AppColor.primaryColor, - AppColor.primaryColor.withOpacity(0.85), - AppColor.primaryColor.withOpacity(0.7), + AppColor.primaryColor.withValues(alpha: 0.85), + AppColor.primaryColor.withValues(alpha: 0.7), ], stops: const [0.0, 0.5, 1.0], ); @@ -78,12 +78,12 @@ class _D { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - AppColor.secondaryColor.withOpacity(0.98), + AppColor.secondaryColor.withValues(alpha: 0.98), AppColor.secondaryColor .withBlue( - (AppColor.secondaryColor.blue + 12).clamp(0, 255), + ((AppColor.secondaryColor.b * 255.0).round() + 12).clamp(0, 255), ) - .withOpacity(0.95), + .withValues(alpha: 0.95), ], ); } @@ -97,12 +97,10 @@ class MainBottomMenuMap extends StatelessWidget { @override Widget build(BuildContext context) { - Get.put(MapPassengerController()); - - return GetBuilder( + return GetBuilder( builder: (controller) { if (controller.isPickerShown) { - return _MapPickerOverlay(controller: controller); + return const _MapPickerOverlay(); } return Positioned( @@ -112,7 +110,6 @@ class MainBottomMenuMap extends StatelessWidget { child: AnimatedContainer( duration: _D.medium, curve: Curves.easeOutQuint, - // تم استبدال الارتفاع الثابت بـ BoxConstraints للسماح بالتمدد الديناميكي constraints: BoxConstraints( maxHeight: controller.isMainBottomMenuMap ? Get.height * 0.4 @@ -124,19 +121,18 @@ class MainBottomMenuMap extends StatelessWidget { boxShadow: _D.cardShadow, border: Border.all( color: Get.isDarkMode - ? Colors.white.withOpacity(0.15) - : Colors.white.withOpacity(0.65), + ? Colors.white.withValues(alpha: 0.15) + : Colors.white.withValues(alpha: 0.65), width: 1.2, ), ), child: ClipRRect( borderRadius: BorderRadius.circular(_D.radiusCard), - // تفعيل السحب والنزول child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: controller.isMainBottomMenuMap - ? _CollapsedView(controller: controller) - : _ExpandedView(controller: controller, context: context), + ? const _CollapsedView() + : const _ExpandedView(), ), ), ), @@ -151,123 +147,128 @@ class MainBottomMenuMap extends StatelessWidget { // ───────────────────────────────────────────────────────────────────────────── class _CollapsedView extends StatelessWidget { - final MapPassengerController controller; - const _CollapsedView({required this.controller}); + const _CollapsedView(); @override Widget build(BuildContext context) { final String firstName = box.read(BoxName.name).toString().split(' ').first; + final mapEngine = Get.find(); + final rideLifecycle = Get.find(); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 14), - AnimatedContainer( - duration: _D.fast, - width: 44, - height: 5, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.grey.shade400.withOpacity(0.6), - Colors.grey.shade300, - Colors.grey.shade400.withOpacity(0.6), - ], - ), - borderRadius: BorderRadius.circular(3), - ), - ), - const SizedBox(height: 16), - Semantics( - button: true, - label: 'Open destination search'.tr, - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: controller.changeMainBottomMenuMap, - borderRadius: BorderRadius.circular(_D.radiusInner), - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 18, vertical: 8), - child: Row( - children: [ - AnimatedContainer( - duration: _D.medium, - width: 48, - height: 48, - decoration: BoxDecoration( - gradient: _D.primaryGradient(), - borderRadius: BorderRadius.circular(_D.radiusPill), - boxShadow: _D.glowShadow(AppColor.primaryColor), - ), - child: const Icon(Icons.search_rounded, - color: Colors.white, size: 22), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text.rich( - TextSpan( - children: [ - TextSpan( - text: '${'Where to'.tr} ', - style: AppStyle.title.copyWith( - fontWeight: FontWeight.w600, - fontSize: 16, - color: Colors.grey.shade700, - ), - ), - TextSpan( - text: firstName, - style: AppStyle.title.copyWith( - fontWeight: FontWeight.w800, - fontSize: 16.5, - color: AppColor.primaryColor, - letterSpacing: -0.3, - ), - ), - const TextSpan(text: '؟'), - ], - ), - ), - const SizedBox(height: 2), - if (!controller.noCarString) - Text( - 'Tap to search your destination'.tr, - style: AppStyle.subtitle.copyWith( - fontSize: 12, - color: Colors.grey.shade500, - fontWeight: FontWeight.w400, - ), - ), - ], - ), - ), + return GetBuilder( + builder: (locationSearch) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 14), + AnimatedContainer( + duration: _D.fast, + width: 44, + height: 5, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.grey.shade400.withValues(alpha: 0.6), + Colors.grey.shade300, + Colors.grey.shade400.withValues(alpha: 0.6), ], ), + borderRadius: BorderRadius.circular(3), + ), + ), + const SizedBox(height: 16), + Semantics( + button: true, + label: 'Open destination search'.tr, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: mapEngine.changeMainBottomMenuMap, + borderRadius: BorderRadius.circular(_D.radiusInner), + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 18, vertical: 8), + child: Row( + children: [ + AnimatedContainer( + duration: _D.medium, + width: 48, + height: 48, + decoration: BoxDecoration( + gradient: _D.primaryGradient(), + borderRadius: BorderRadius.circular(_D.radiusPill), + boxShadow: _D.glowShadow(AppColor.primaryColor), + ), + child: const Icon(Icons.search_rounded, + color: Colors.white, size: 22), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${'Where to'.tr} ', + style: AppStyle.title.copyWith( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.grey.shade700, + ), + ), + TextSpan( + text: firstName, + style: AppStyle.title.copyWith( + fontWeight: FontWeight.w800, + fontSize: 16.5, + color: AppColor.primaryColor, + letterSpacing: -0.3, + ), + ), + const TextSpan(text: '؟'), + ], + ), + ), + const SizedBox(height: 2), + if (!rideLifecycle.noCarString) + Text( + 'Tap to search your destination'.tr, + style: AppStyle.subtitle.copyWith( + fontSize: 12, + color: Colors.grey.shade500, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ], + ), + ), + ), ), ), - ), - ), - if (controller.recentPlaces.isNotEmpty) ...[ - const SizedBox(height: 12), - Container( - height: 40, - padding: const EdgeInsets.symmetric(horizontal: 18), - child: ListView.separated( - scrollDirection: Axis.horizontal, - itemCount: controller.recentPlaces.length, - separatorBuilder: (_, __) => const SizedBox(width: 10), - itemBuilder: (context, index) => - _RecentPlaceChip(controller: controller, index: index), - ), - ), - const SizedBox(height: 16), - ] else - const SizedBox(height: 20), - ], + if (locationSearch.recentPlaces.isNotEmpty) ...[ + const SizedBox(height: 12), + Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 18), + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: locationSearch.recentPlaces.length, + separatorBuilder: (_, __) => const SizedBox(width: 10), + itemBuilder: (context, index) => + _RecentPlaceChip(locationSearch: locationSearch, index: index), + ), + ), + const SizedBox(height: 16), + ] else + const SizedBox(height: 20), + ], + ); + }, ); } } @@ -277,256 +278,261 @@ class _CollapsedView extends StatelessWidget { // ───────────────────────────────────────────────────────────────────────────── class _ExpandedView extends StatelessWidget { - final MapPassengerController controller; - final BuildContext context; - const _ExpandedView({required this.controller, required this.context}); + const _ExpandedView(); @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 14), - Center( - child: AnimatedContainer( - duration: _D.fast, - width: 44, - height: 5, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Colors.grey.shade400.withOpacity(0.6), - Colors.grey.shade300, - Colors.grey.shade400.withOpacity(0.6), - ], - ), - borderRadius: BorderRadius.circular(3), - ), - ), - ), + final mapEngine = Get.find(); + final rideLifecycle = Get.find(); - // ── Header ── - Container( - padding: const EdgeInsets.fromLTRB(20, 18, 16, 14), - child: Row( - children: [ - Text( - 'Plan Your Route'.tr, - style: AppStyle.title.copyWith( - fontWeight: FontWeight.w800, - fontSize: 18, - letterSpacing: -0.5, - ), - ), - const Spacer(), - Semantics( - button: true, - label: 'Close panel'.tr, - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: controller.changeMainBottomMenuMap, - borderRadius: BorderRadius.circular(_D.radiusPill), - child: Container( - width: 38, - height: 38, - decoration: BoxDecoration( - color: Colors.grey.shade100, - shape: BoxShape.circle, - ), - child: Icon(Icons.keyboard_arrow_down_rounded, - size: 24, color: Colors.grey.shade600), - ), + return GetBuilder( + builder: (locationSearch) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 14), + Center( + child: AnimatedContainer( + duration: _D.fast, + width: 44, + height: 5, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.grey.shade400.withValues(alpha: 0.6), + Colors.grey.shade300, + Colors.grey.shade400.withValues(alpha: 0.6), + ], ), + borderRadius: BorderRadius.circular(3), ), ), - ], - ), - ), + ), - // ── Group 1: Core Routing ── - _buildSectionTitle('Route'.tr), - - _buildTimelineItem( - dotColor: AppColor.primaryColor, - showTopLine: false, - showBottomLine: true, - isStart: true, - child: !controller.isAnotherOreder - ? _TimelineRow( - icon: Icons.my_location_rounded, - iconColor: AppColor.primaryColor, - bgColor: AppColor.primaryColor, - label: controller.currentLocationString, - ) - : Padding( - padding: const EdgeInsets.only(right: 16), - child: formSearchPlacesStart(), - ), - ), - - ...List.generate(controller.activeMenuWaypointCount, (index) { - final wpName = controller.menuWaypointNames[index]; - final isSet = controller.menuWaypoints[index] != null; - final Color accent = - index == 0 ? Colors.amber.shade600 : Colors.deepPurple.shade400; - final Color soft = - index == 0 ? Colors.amber.shade50 : Colors.deepPurple.shade50; - - return _buildTimelineItem( - dotColor: accent, - showTopLine: true, - showBottomLine: true, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [soft.withOpacity(0.9), soft.withOpacity(0.6)]), - borderRadius: BorderRadius.circular(_D.radiusInner), - border: Border.all( - color: isSet - ? accent.withOpacity(0.35) - : Colors.grey.shade200), - ), + // ── Header ── + Container( + padding: const EdgeInsets.fromLTRB(20, 18, 16, 14), child: Row( children: [ - Container( - width: 26, - height: 26, - decoration: - BoxDecoration(color: accent, shape: BoxShape.circle), - child: Center( - child: Text('${index + 1}', - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.w800))), - ), - const SizedBox(width: 12), - Expanded( - child: GestureDetector( - onTap: () { - controller.changeMainBottomMenuMap(); - controller.startPickingWaypointOnMap(index); - }, - child: Text( - isSet ? wpName : '${'Stop'.tr} ${index + 1}', - style: TextStyle( - fontSize: 13.5, - color: isSet - ? accent.withOpacity(0.9) - : Colors.grey.shade400, - fontWeight: isSet ? FontWeight.w600 : FontWeight.w400, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + Text( + 'Plan Your Route'.tr, + style: AppStyle.title.copyWith( + fontWeight: FontWeight.w800, + fontSize: 18, + letterSpacing: -0.5, ), - )), - GestureDetector( - onTap: () => controller.removeMenuWaypoint(index), - child: Container( - width: 28, - height: 28, - decoration: BoxDecoration( - color: Colors.red.shade50, shape: BoxShape.circle), - child: Icon(Icons.close_rounded, - color: Colors.red.shade400, size: 15), + ), + const Spacer(), + Semantics( + button: true, + label: 'Close panel'.tr, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: mapEngine.changeMainBottomMenuMap, + borderRadius: BorderRadius.circular(_D.radiusPill), + child: Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: Colors.grey.shade100, + shape: BoxShape.circle, + ), + child: Icon(Icons.keyboard_arrow_down_rounded, + size: 24, color: Colors.grey.shade600), + ), + ), ), ), ], ), ), - ); - }), - if (controller.activeMenuWaypointCount < 2) - _buildTimelineItem( - dotColor: Colors.orange.shade300, - isDotDashed: true, - showTopLine: true, - showBottomLine: true, - child: InkWell( - onTap: () => controller.addMenuWaypoint(), - borderRadius: BorderRadius.circular(_D.radiusInner), - child: Container( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(_D.radiusInner), - border: Border.all(color: Colors.orange.shade200, width: 1.5), - color: Colors.orange.shade50.withOpacity(0.6), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.add_location_alt_outlined, - color: Colors.orange.shade500, size: 18), - const SizedBox(width: 10), - Text('Add a Stop'.tr, - style: TextStyle( - color: Colors.orange.shade700, + // ── Group 1: Core Routing ── + _buildSectionTitle('Route'.tr), + + _buildTimelineItem( + dotColor: AppColor.primaryColor, + showTopLine: false, + showBottomLine: true, + isStart: true, + child: !rideLifecycle.isAnotherOreder + ? _TimelineRow( + icon: Icons.my_location_rounded, + iconColor: AppColor.primaryColor, + bgColor: AppColor.primaryColor, + label: locationSearch.currentLocationString, + ) + : Padding( + padding: const EdgeInsets.only(right: 16), + child: formSearchPlacesStart(), + ), + ), + + ...List.generate(locationSearch.activeMenuWaypointCount, (index) { + final wpName = locationSearch.menuWaypointNames[index]; + final isSet = locationSearch.menuWaypoints[index] != null; + final Color accent = + index == 0 ? Colors.amber.shade600 : Colors.deepPurple.shade400; + final Color soft = + index == 0 ? Colors.amber.shade50 : Colors.deepPurple.shade50; + + return _buildTimelineItem( + dotColor: accent, + showTopLine: true, + showBottomLine: true, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [soft.withValues(alpha: 0.9), soft.withValues(alpha: 0.6)]), + borderRadius: BorderRadius.circular(_D.radiusInner), + border: Border.all( + color: isSet + ? accent.withValues(alpha: 0.35) + : Colors.grey.shade200), + ), + child: Row( + children: [ + Container( + width: 26, + height: 26, + decoration: + BoxDecoration(color: accent, shape: BoxShape.circle), + child: Center( + child: Text('${index + 1}', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w800))), + ), + const SizedBox(width: 12), + Expanded( + child: GestureDetector( + onTap: () { + mapEngine.changeMainBottomMenuMap(); + locationSearch.startPickingWaypointOnMap(index); + }, + child: Text( + isSet ? wpName : '${'Stop'.tr} ${index + 1}', + style: TextStyle( fontSize: 13.5, - fontWeight: FontWeight.w600)), - ], + color: isSet + ? accent.withValues(alpha: 0.9) + : Colors.grey.shade400, + fontWeight: isSet ? FontWeight.w600 : FontWeight.w400, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + )), + GestureDetector( + onTap: () => locationSearch.removeMenuWaypoint(index), + child: Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: Colors.red.shade50, shape: BoxShape.circle), + child: Icon(Icons.close_rounded, + color: Colors.red.shade400, size: 15), + ), + ), + ], + ), + ), + ); + }), + + if (locationSearch.activeMenuWaypointCount < 2) + _buildTimelineItem( + dotColor: Colors.orange.shade300, + isDotDashed: true, + showTopLine: true, + showBottomLine: true, + child: InkWell( + onTap: () => locationSearch.addMenuWaypoint(), + borderRadius: BorderRadius.circular(_D.radiusInner), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(_D.radiusInner), + border: Border.all(color: Colors.orange.shade200, width: 1.5), + color: Colors.orange.shade50.withValues(alpha: 0.6), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.add_location_alt_outlined, + color: Colors.orange.shade500, size: 18), + const SizedBox(width: 10), + Text('Add a Stop'.tr, + style: TextStyle( + color: Colors.orange.shade700, + fontSize: 13.5, + fontWeight: FontWeight.w600)), + ], + ), + ), ), ), + + _buildTimelineItem( + dotColor: Colors.red.shade500, + showTopLine: true, + showBottomLine: false, + isEnd: true, + child: Padding( + padding: const EdgeInsets.only(right: 16), + child: formSearchPlacesDestenation(), + ), ), - ), - _buildTimelineItem( - dotColor: Colors.red.shade500, - showTopLine: true, - showBottomLine: false, - isEnd: true, - child: Padding( - padding: const EdgeInsets.only(right: 16), - child: formSearchPlacesDestenation(), - ), - ), + const SizedBox(height: 16), - const SizedBox(height: 16), + // ── Group 2: Quick Access ── + _buildSectionDivider(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildSectionTitle('Quick Access'.tr), + const FaviouratePlacesDialog(), // تم نقلها هنا لتكون جزء من الوصول السريع + ], + ), - // ── Group 2: Quick Access ── - _buildSectionDivider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildSectionTitle('Quick Access'.tr), - const FaviouratePlacesDialog(), // تم نقلها هنا لتكون جزء من الوصول السريع + if (locationSearch.recentPlaces.isNotEmpty) + Container( + height: 40, + margin: const EdgeInsets.only(bottom: 16), + child: ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 20), + scrollDirection: Axis.horizontal, + itemCount: locationSearch.recentPlaces.length, + separatorBuilder: (_, __) => const SizedBox(width: 10), + itemBuilder: (context, index) => + _RecentPlaceChip(locationSearch: locationSearch, index: index), + ), + ), + + // ── Group 3: Advanced Tools ── + _buildSectionDivider(), + _buildSectionTitle('Advanced Tools'.tr), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: _WhatsAppLinkButton(locationSearch: locationSearch), + ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: _OrderTypeButton(mapEngine: mapEngine), + ), + + const SizedBox(height: 24), // مساحة سفلية لضمان راحة السحب ], - ), - - if (controller.recentPlaces.isNotEmpty) - Container( - height: 40, - margin: const EdgeInsets.only(bottom: 16), - child: ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 20), - scrollDirection: Axis.horizontal, - itemCount: controller.recentPlaces.length, - separatorBuilder: (_, __) => const SizedBox(width: 10), - itemBuilder: (context, index) => - _RecentPlaceChip(controller: controller, index: index), - ), - ), - - // ── Group 3: Advanced Tools ── - _buildSectionDivider(), - _buildSectionTitle('Advanced Tools'.tr), - - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18), - child: _WhatsAppLinkButton(controller: controller), - ), - const SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18), - child: _OrderTypeButton(controller: controller), - ), - - const SizedBox(height: 24), // مساحة سفلية لضمان راحة السحب - ], + ); + }, ); } @@ -603,256 +609,269 @@ class _ExpandedView extends StatelessWidget { // ───────────────────────────────────────────────────────────────────────────── class _MapPickerOverlay extends StatelessWidget { - final MapPassengerController controller; - const _MapPickerOverlay({required this.controller}); + const _MapPickerOverlay(); - String _getModeTitle(BuildContext context) { - if (controller.isPickingWaypoint) { + String _getModeTitle(LocationSearchController locationSearch, BuildContext context) { + if (locationSearch.isPickingWaypoint) { return 'Move map to set stop'.tr + - ' ${controller.pickingWaypointIndex + 1}'.tr; + ' ${locationSearch.pickingWaypointIndex + 1}'.tr; } - if (controller.passengerStartLocationFromMap) { - return controller.isAnotherOreder + if (locationSearch.passengerStartLocationFromMap) { + final rideLifecycle = Get.find(); + return rideLifecycle.isAnotherOreder ? 'Now set the pickup point for the other person'.tr : 'Move map to your pickup point'.tr; } - if (controller.startLocationFromMap) { + if (locationSearch.startLocationFromMap) { return 'Move map to set start location'.tr; } - if (controller.workLocationFromMap) { + if (locationSearch.workLocationFromMap) { return 'Move map to your work location'.tr; } - if (controller.homeLocationFromMap) { + if (locationSearch.homeLocationFromMap) { return 'Move map to your home location'.tr; } return 'Move map to select destination'.tr; } - String _getConfirmLabel(BuildContext context) { - if (controller.isPickingWaypoint) return 'Set as Stop'.tr; - if (controller.passengerStartLocationFromMap) { + String _getConfirmLabel(LocationSearchController locationSearch, BuildContext context) { + if (locationSearch.isPickingWaypoint) return 'Set as Stop'.tr; + if (locationSearch.passengerStartLocationFromMap) { return 'Confirm Pickup Location'.tr; } - if (controller.workLocationFromMap) return 'Set as Work'.tr; - if (controller.homeLocationFromMap) return 'Set as Home'.tr; + if (locationSearch.workLocationFromMap) return 'Set as Work'.tr; + if (locationSearch.homeLocationFromMap) return 'Set as Home'.tr; return 'Set Destination'.tr; } - IconData _getModeIcon() { - if (controller.isPickingWaypoint) return Icons.add_location_alt_rounded; - if (controller.passengerStartLocationFromMap) { + IconData _getModeIcon(LocationSearchController locationSearch) { + if (locationSearch.isPickingWaypoint) return Icons.add_location_alt_rounded; + if (locationSearch.passengerStartLocationFromMap) { return Icons.person_pin_circle_rounded; } - if (controller.workLocationFromMap) return Icons.work_rounded; - if (controller.homeLocationFromMap) return Icons.home_rounded; + if (locationSearch.workLocationFromMap) return Icons.work_rounded; + if (locationSearch.homeLocationFromMap) return Icons.home_rounded; return Icons.location_on_rounded; } - Color _getModeColor() { - if (controller.isPickingWaypoint) return Colors.orange.shade600; - if (controller.passengerStartLocationFromMap) return Colors.green.shade600; - if (controller.workLocationFromMap) return Colors.blue.shade600; - if (controller.homeLocationFromMap) return Colors.orange.shade600; + Color _getModeColor(LocationSearchController locationSearch) { + if (locationSearch.isPickingWaypoint) return Colors.orange.shade600; + if (locationSearch.passengerStartLocationFromMap) return Colors.green.shade600; + if (locationSearch.workLocationFromMap) return Colors.blue.shade600; + if (locationSearch.homeLocationFromMap) return Colors.orange.shade600; return AppColor.primaryColor; } @override Widget build(BuildContext context) { - final modeColor = _getModeColor(); + final mapEngine = Get.find(); - return Positioned( - bottom: Get.height * .035, - left: 16, - right: 16, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), - decoration: BoxDecoration( - color: modeColor, - borderRadius: BorderRadius.circular(_D.radiusCard), - ), - child: Row( - children: [ - Icon(_getModeIcon(), color: Colors.white, size: 19), - const SizedBox(width: 14), - Expanded( - child: Text( - _getModeTitle(context), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w700, - fontSize: 14), - ), + return GetBuilder( + builder: (locationSearch) { + final modeColor = _getModeColor(locationSearch); + + return Positioned( + bottom: Get.height * .035, + left: 16, + right: 16, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + decoration: BoxDecoration( + color: modeColor, + borderRadius: BorderRadius.circular(_D.radiusCard), ), - ], - ), + child: Row( + children: [ + Icon(_getModeIcon(locationSearch), color: Colors.white, size: 19), + const SizedBox(width: 14), + Expanded( + child: Text( + _getModeTitle(locationSearch, context), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + fontSize: 14), + ), + ), + ], + ), + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: AppColor.secondaryColor, + borderRadius: BorderRadius.circular(_D.radiusCard), + boxShadow: _D.cardShadow, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 18, 20, 2), + child: Row( + children: [ + Icon(Icons.gps_fixed_rounded, color: modeColor, size: 16), + const SizedBox(width: 14), + Expanded( + child: Text( + '${locationSearch.newMyLocation.latitude.toStringAsFixed(5)}, ${locationSearch.newMyLocation.longitude.toStringAsFixed(5)}', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade700, + fontWeight: FontWeight.w500), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 18), + child: Row( + children: [ + Expanded( + flex: 2, + child: OutlinedButton( + onPressed: () { + mapEngine.isPickerShown = false; + locationSearch.passengerStartLocationFromMap = false; + locationSearch.startLocationFromMap = false; + locationSearch.workLocationFromMap = false; + locationSearch.homeLocationFromMap = false; + locationSearch.isPickingWaypoint = false; + locationSearch.pickingWaypointIndex = -1; + if (!mapEngine.isMainBottomMenuMap) { + mapEngine.isMainBottomMenuMap = true; + mapEngine.mainBottomMenuMapHeight = + Get.height * .22; + } + mapEngine.update(); + locationSearch.update(); + }, + child: Text('Cancel'.tr), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 3, + child: ElevatedButton( + onPressed: () => _onConfirmTap(mapEngine, locationSearch, context), + style: ElevatedButton.styleFrom( + backgroundColor: modeColor), + child: Text(_getConfirmLabel(locationSearch, context), + style: const TextStyle(color: Colors.white)), + ), + ), + ], + ), + ), + ], + ), + ), + ], ), - const SizedBox(height: 12), - Container( - decoration: BoxDecoration( - color: AppColor.secondaryColor, - borderRadius: BorderRadius.circular(_D.radiusCard), - boxShadow: _D.cardShadow, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(20, 18, 20, 2), - child: Row( - children: [ - Icon(Icons.gps_fixed_rounded, color: modeColor, size: 16), - const SizedBox(width: 14), - Expanded( - child: Text( - '${controller.newMyLocation.latitude.toStringAsFixed(5)}, ${controller.newMyLocation.longitude.toStringAsFixed(5)}', - style: TextStyle( - fontSize: 12, - color: Colors.grey.shade700, - fontWeight: FontWeight.w500), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.fromLTRB(16, 14, 16, 18), - child: Row( - children: [ - Expanded( - flex: 2, - child: OutlinedButton( - onPressed: () { - controller.isPickerShown = false; - controller.passengerStartLocationFromMap = false; - controller.startLocationFromMap = false; - controller.workLocationFromMap = false; - controller.homeLocationFromMap = false; - controller.isPickingWaypoint = false; - controller.pickingWaypointIndex = -1; - if (!controller.isMainBottomMenuMap) { - controller.isMainBottomMenuMap = true; - controller.mainBottomMenuMapHeight = - Get.height * .22; - } - controller.update(); - }, - child: Text('Cancel'.tr), - ), - ), - const SizedBox(width: 12), - Expanded( - flex: 3, - child: ElevatedButton( - onPressed: () => _onConfirmTap(controller, context), - style: ElevatedButton.styleFrom( - backgroundColor: modeColor), - child: Text(_getConfirmLabel(context), - style: const TextStyle(color: Colors.white)), - ), - ), - ], - ), - ), - ], - ), - ), - ], - ), + ); + }, ); } Future _onConfirmTap( - MapPassengerController controller, BuildContext context) async { + MapEngineController mapEngine, LocationSearchController locationSearch, BuildContext context) async { + final rideLifecycle = Get.find(); Log.print( - '🔘 _onConfirmTap: isPickingWaypoint=${controller.isPickingWaypoint}, newMyLocation=${controller.newMyLocation}'); + '🔘 _onConfirmTap: isPickingWaypoint=${locationSearch.isPickingWaypoint}, newMyLocation=${locationSearch.newMyLocation}'); await Future.delayed(const Duration(milliseconds: 280)); final LatLng currentCameraPosition = LatLng( - controller.newMyLocation.latitude, controller.newMyLocation.longitude); + locationSearch.newMyLocation.latitude, locationSearch.newMyLocation.longitude); - if (controller.isPickingWaypoint && controller.pickingWaypointIndex >= 0) { - controller.setMenuWaypointFromMap( - controller.pickingWaypointIndex, currentCameraPosition); + if (locationSearch.isPickingWaypoint && locationSearch.pickingWaypointIndex >= 0) { + locationSearch.setMenuWaypointFromMap( + locationSearch.pickingWaypointIndex, currentCameraPosition); mySnackbarSuccess('Waypoint has been set successfully'.tr); return; } - controller.clearPolyline(); - controller.data = []; + mapEngine.clearPolyline(); + rideLifecycle.data = []; - if (controller.passengerStartLocationFromMap) { + if (locationSearch.passengerStartLocationFromMap) { final LatLng start = currentCameraPosition; - controller.newStartPointLocation = start; - controller.passengerStartLocationFromMap = false; - controller.isPickerShown = false; - controller.currentLocationToFormPlaces = false; - controller.placesDestination = []; - controller.clearPlacesStart(); - controller.clearPlacesDestination(); - controller.isMainBottomMenuMap = true; - controller.mainBottomMenuMapHeight = Get.height * .22; - controller.update(); - await controller.getDirectionMap('${start.latitude},${start.longitude}', - '${controller.myDestination.latitude},${controller.myDestination.longitude}'); - controller.showBottomSheet1(); + locationSearch.newStartPointLocation = start; + locationSearch.passengerStartLocationFromMap = false; + mapEngine.isPickerShown = false; + locationSearch.currentLocationToFormPlaces = false; + locationSearch.placesDestination = []; + locationSearch.clearPlacesStart(); + locationSearch.clearPlacesDestination(); + mapEngine.isMainBottomMenuMap = true; + mapEngine.mainBottomMenuMapHeight = Get.height * .22; + mapEngine.update(); + locationSearch.update(); + await rideLifecycle.getDirectionMap('${start.latitude},${start.longitude}', + '${locationSearch.myDestination.latitude},${locationSearch.myDestination.longitude}'); + rideLifecycle.showBottomSheet1(); return; } - if (controller.startLocationFromMap) { + if (locationSearch.startLocationFromMap) { final LatLng start = currentCameraPosition; - controller.newMyLocation = start; - controller.newStartPointLocation = start; - controller.hintTextStartPoint = + locationSearch.newMyLocation = start; + locationSearch.newStartPointLocation = start; + locationSearch.hintTextStartPoint = '${start.latitude.toStringAsFixed(4)} , ${start.longitude.toStringAsFixed(4)}'; - controller.startLocationFromMap = false; - controller.isPickerShown = false; - controller.update(); + locationSearch.startLocationFromMap = false; + mapEngine.isPickerShown = false; + locationSearch.update(); + mapEngine.update(); return; } - if (controller.workLocationFromMap) { + if (locationSearch.workLocationFromMap) { box.write(BoxName.addWork, '${currentCameraPosition.latitude.toStringAsFixed(4)} , ${currentCameraPosition.longitude.toStringAsFixed(4)}'); - controller.hintTextDestinationPoint = 'To Work'.tr; - controller.workLocationFromMap = false; - controller.isPickerShown = false; - controller.update(); + locationSearch.hintTextDestinationPoint = 'To Work'.tr; + locationSearch.workLocationFromMap = false; + mapEngine.isPickerShown = false; + locationSearch.update(); + mapEngine.update(); mySnackbarSuccess('Work Saved'.tr); return; } - if (controller.homeLocationFromMap) { + if (locationSearch.homeLocationFromMap) { box.write(BoxName.addHome, '${currentCameraPosition.latitude.toStringAsFixed(4)} , ${currentCameraPosition.longitude.toStringAsFixed(4)}'); - controller.hintTextDestinationPoint = 'To Home'.tr; - controller.homeLocationFromMap = false; - controller.isPickerShown = false; - controller.update(); + locationSearch.hintTextDestinationPoint = 'To Home'.tr; + locationSearch.homeLocationFromMap = false; + mapEngine.isPickerShown = false; + locationSearch.update(); + mapEngine.update(); mySnackbarSuccess('Home Saved'.tr); return; } - controller.myDestination = currentCameraPosition; - controller.hintTextDestinationPoint = + locationSearch.myDestination = currentCameraPosition; + locationSearch.hintTextDestinationPoint = '${currentCameraPosition.latitude.toStringAsFixed(4)} , ${currentCameraPosition.longitude.toStringAsFixed(4)}'; - controller.placesDestination = []; - controller.placeDestinationController.clear(); - controller.passengerStartLocationFromMap = true; - controller.isPickerShown = true; - controller.update(); + locationSearch.placesDestination = []; + locationSearch.placeDestinationController.clear(); + locationSearch.passengerStartLocationFromMap = true; + mapEngine.isPickerShown = true; + locationSearch.update(); + mapEngine.update(); try { - if (controller.isAnotherOreder) { - await controller.mapController?.animateCamera(CameraUpdate.newLatLng( - LatLng(controller.newStartPointLocation.latitude, - controller.newStartPointLocation.longitude))); + if (rideLifecycle.isAnotherOreder) { + await mapEngine.mapController?.animateCamera(CameraUpdate.newLatLng( + LatLng(locationSearch.newStartPointLocation.latitude, + locationSearch.newStartPointLocation.longitude))); } else { - await controller.mapController?.animateCamera(CameraUpdate.newLatLng( - LatLng(controller.passengerLocation.latitude, - controller.passengerLocation.longitude))); + await mapEngine.mapController?.animateCamera(CameraUpdate.newLatLng( + LatLng(locationSearch.passengerLocation.latitude, + locationSearch.passengerLocation.longitude))); } } catch (e) { Log.print("Error occurred: $e"); @@ -880,7 +899,7 @@ class _TimelineRow extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11), decoration: BoxDecoration( - color: bgColor.withAlpha(15), + color: bgColor.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(_D.radiusInner)), child: Row( children: [ @@ -899,13 +918,14 @@ class _TimelineRow extends StatelessWidget { } class _RecentPlaceChip extends StatelessWidget { - final MapPassengerController controller; + final LocationSearchController locationSearch; final int index; - const _RecentPlaceChip({required this.controller, required this.index}); + const _RecentPlaceChip({required this.locationSearch, required this.index}); @override Widget build(BuildContext context) { - final place = controller.recentPlaces[index]; + final place = locationSearch.recentPlaces[index]; + final rideLifecycle = Get.find(); return Material( color: Colors.transparent, child: InkWell( @@ -914,11 +934,11 @@ class _RecentPlaceChip extends StatelessWidget { 'Are you want to go this site'.tr, ' ', () async { - await controller.getLocation(); - await controller.getDirectionMap( - '${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}', + await locationSearch.getLocation(); + await rideLifecycle.getDirectionMap( + '${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}', '${place['latitude']},${place['longitude']}'); - controller.showBottomSheet1(); + rideLifecycle.showBottomSheet1(); }, ); }, @@ -926,19 +946,19 @@ class _RecentPlaceChip extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 7), decoration: BoxDecoration( - color: AppColor.primaryColor.withOpacity(0.08), + color: AppColor.primaryColor.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(_D.radiusChip), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.history_rounded, - size: 14, color: AppColor.primaryColor.withOpacity(0.7)), + size: 14, color: AppColor.primaryColor.withValues(alpha: 0.7)), const SizedBox(width: 7), Text(place['name'] ?? '', style: TextStyle( fontSize: 12.5, - color: AppColor.primaryColor.withOpacity(0.9), + color: AppColor.primaryColor.withValues(alpha: 0.9), fontWeight: FontWeight.w600)), ], ), @@ -949,8 +969,8 @@ class _RecentPlaceChip extends StatelessWidget { } class _WhatsAppLinkButton extends StatelessWidget { - final MapPassengerController controller; - const _WhatsAppLinkButton({required this.controller}); + final LocationSearchController locationSearch; + const _WhatsAppLinkButton({required this.locationSearch}); @override Widget build(BuildContext context) { @@ -964,19 +984,19 @@ class _WhatsAppLinkButton extends StatelessWidget { borderRadius: BorderRadius.circular(22)), title: Text('WhatsApp Location Extractor'.tr), content: Form( - key: controller.sosFormKey, + key: locationSearch.sosFormKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ MyTextForm( - controller: controller.whatsAppLocationText, + controller: locationSearch.whatsAppLocationText, label: 'Location Link'.tr, type: TextInputType.url, hint: 'https://maps.app.goo.gl/...'), const SizedBox(height: 16), MyElevatedButton( title: 'Go to this location'.tr, - onPressed: () => controller.goToWhatappLocation()), + onPressed: () => locationSearch.goToWhatappLocation()), ], ), ), @@ -1008,12 +1028,13 @@ class _WhatsAppLinkButton extends StatelessWidget { } class _OrderTypeButton extends StatelessWidget { - final MapPassengerController controller; - const _OrderTypeButton({required this.controller}); + final MapEngineController mapEngine; + const _OrderTypeButton({required this.mapEngine}); @override Widget build(BuildContext context) { - final bool isOther = controller.isAnotherOreder; + final rideLifecycle = Get.find(); + final bool isOther = mapEngine.isAnotherOreder; final Color accent = isOther ? Colors.indigo.shade500 : AppColor.primaryColor; @@ -1029,14 +1050,16 @@ class _OrderTypeButton extends StatelessWidget { CupertinoActionSheetAction( child: Text('I want to order for myself'.tr), onPressed: () { - controller.changeisAnotherOreder(false); + mapEngine.changeisAnotherOreder(false); + rideLifecycle.isAnotherOreder = false; Navigator.pop(ctx); }, ), CupertinoActionSheetAction( child: Text('I want to order for someone else'.tr), onPressed: () { - controller.changeisAnotherOreder(true); + mapEngine.changeisAnotherOreder(true); + rideLifecycle.isAnotherOreder = true; Navigator.pop(ctx); }, ), @@ -1052,7 +1075,7 @@ class _OrderTypeButton extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( - color: accent.withOpacity(0.08), + color: accent.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(_D.radiusInner)), child: Row( children: [ @@ -1081,67 +1104,69 @@ class FaviouratePlacesDialog extends StatelessWidget { @override Widget build(BuildContext context) { - Get.put(MapPassengerController()); - return GetBuilder( - builder: (controller) => InkWell( - borderRadius: BorderRadius.circular(14), - onTap: () async { - final List favoritePlaces = - await sql.getAllData(TableName.placesFavorite); - Get.defaultDialog( - title: 'Favorite Places'.tr, - content: SizedBox( - width: Get.width * .85, - height: 300, - child: favoritePlaces.isEmpty - ? Center(child: Text('No favorite places yet!'.tr)) - : ListView.separated( - itemCount: favoritePlaces.length, - separatorBuilder: (_, __) => - Divider(height: 1, color: Colors.grey.shade100), - itemBuilder: (context, index) => ListTile( - leading: const Icon(Icons.star, - color: Colors.amber, size: 19), - title: Text(favoritePlaces[index]['name']), - trailing: IconButton( - icon: const Icon(Icons.delete_outline, - color: Colors.redAccent), - onPressed: () async { - await sql.deleteData(TableName.placesFavorite, - favoritePlaces[index]['id']); + return GetBuilder( + builder: (locationSearch) { + final rideLifecycle = Get.find(); + return InkWell( + borderRadius: BorderRadius.circular(14), + onTap: () async { + final List favoritePlaces = + await sql.getAllData(TableName.placesFavorite); + Get.defaultDialog( + title: 'Favorite Places'.tr, + content: SizedBox( + width: Get.width * .85, + height: 300, + child: favoritePlaces.isEmpty + ? Center(child: Text('No favorite places yet!'.tr)) + : ListView.separated( + itemCount: favoritePlaces.length, + separatorBuilder: (_, __) => + Divider(height: 1, color: Colors.grey.shade100), + itemBuilder: (context, index) => ListTile( + leading: const Icon(Icons.star, + color: Colors.amber, size: 19), + title: Text(favoritePlaces[index]['name']), + trailing: IconButton( + icon: const Icon(Icons.delete_outline, + color: Colors.redAccent), + onPressed: () async { + await sql.deleteData(TableName.placesFavorite, + favoritePlaces[index]['id']); + Get.back(); + }, + ), + onTap: () async { Get.back(); + await locationSearch.getLocation(); + await rideLifecycle.getDirectionMap( + '${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}', + '${favoritePlaces[index]['latitude']},${favoritePlaces[index]['longitude']}'); + rideLifecycle.showBottomSheet1(); }, ), - onTap: () async { - Get.back(); - await controller.getLocation(); - await controller.getDirectionMap( - '${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}', - '${favoritePlaces[index]['latitude']},${favoritePlaces[index]['longitude']}'); - controller.showBottomSheet1(); - }, ), - ), + ), + confirm: + MyElevatedButton(title: 'Back'.tr, onPressed: () => Get.back()), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star_border_rounded, + color: AppColor.accentColor, size: 21), + const SizedBox(width: 10), + Text('Favorite Places'.tr, + style: AppStyle.title + .copyWith(fontWeight: FontWeight.w600, fontSize: 14)), + ], ), - confirm: - MyElevatedButton(title: 'Back'.tr, onPressed: () => Get.back()), - ); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.star_border_rounded, - color: AppColor.accentColor, size: 21), - const SizedBox(width: 10), - Text('Favorite Places'.tr, - style: AppStyle.title - .copyWith(fontWeight: FontWeight.w600, fontSize: 14)), - ], ), - ), - ), + ); + }, ); } } diff --git a/lib/views/home/map_widget.dart/map_menu_widget.dart b/lib/views/home/map_widget.dart/map_menu_widget.dart index 88d2df6..f86a862 100644 --- a/lib/views/home/map_widget.dart/map_menu_widget.dart +++ b/lib/views/home/map_widget.dart/map_menu_widget.dart @@ -15,7 +15,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../../constant/colors.dart'; import '../../../constant/links.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/map_engine_controller.dart'; import '../../notification/notification_page.dart'; import '../HomePage/contact_us.dart'; import '../HomePage/share_app_page.dart'; @@ -28,9 +28,8 @@ Color get _kBg => Get.isDarkMode ? const Color(0xFF060B18) : AppColor.secondaryColor; Color get _kBgSurface => Get.isDarkMode ? const Color(0xFF0D1525) - : AppColor.secondaryColor.withOpacity(0.9); + : AppColor.secondaryColor.withValues(alpha: 0.9); const _kAmber = Color(0xFFFFB700); -Color get _kBorder => _kCyan.withOpacity(0.15); Color get _kText => AppColor.writeColor; Color get _kTextMuted => AppColor.grayColor; @@ -39,16 +38,14 @@ class MapMenuWidget extends StatelessWidget { @override Widget build(BuildContext context) { - Get.lazyPut(() => MapPassengerController()); - - return GetBuilder( + return GetBuilder( builder: (controller) => Stack( children: [ // ── تعتيم الخلفية ─────────────────────────────────────────────── if (controller.widthMenu > 0) GestureDetector( onTap: controller.getDrawerMenu, - child: Container(color: Colors.black.withOpacity(0.55)), + child: Container(color: Colors.black.withValues(alpha: 0.55)), ), _buildSideMenu(controller), @@ -59,7 +56,7 @@ class MapMenuWidget extends StatelessWidget { } // ── زر القائمة العائم ──────────────────────────────────────────────────── - Widget _buildMenuButton(MapPassengerController controller) { + Widget _buildMenuButton(MapEngineController controller) { return Positioned( top: 45, left: 16, @@ -76,12 +73,12 @@ class MapMenuWidget extends StatelessWidget { width: 48, height: 48, decoration: BoxDecoration( - color: _kBg.withOpacity(0.88), + color: _kBg.withValues(alpha: 0.88), borderRadius: BorderRadius.circular(16), - border: Border.all(color: _kCyan.withOpacity(0.25), width: 1), + border: Border.all(color: _kCyan.withValues(alpha: 0.25), width: 1), boxShadow: [ BoxShadow( - color: _kCyan.withOpacity(0.12), + color: _kCyan.withValues(alpha: 0.12), blurRadius: 16, ), ], @@ -106,7 +103,7 @@ class MapMenuWidget extends StatelessWidget { } // ── القائمة الجانبية ───────────────────────────────────────────────────── - Widget _buildSideMenu(MapPassengerController controller) { + Widget _buildSideMenu(MapEngineController controller) { return AnimatedPositioned( duration: const Duration(milliseconds: 420), curve: Curves.fastOutSlowIn, @@ -120,13 +117,13 @@ class MapMenuWidget extends StatelessWidget { width: Get.width * 0.8, constraints: const BoxConstraints(maxWidth: 320), decoration: BoxDecoration( - color: _kBg.withOpacity(0.97), + color: _kBg.withValues(alpha: 0.97), border: Border( - right: BorderSide(color: _kCyan.withOpacity(0.12), width: 1), + right: BorderSide(color: _kCyan.withValues(alpha: 0.12), width: 1), ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.5), + color: Colors.black.withValues(alpha: 0.5), blurRadius: 32, ), ], @@ -239,7 +236,7 @@ class MapMenuWidget extends StatelessWidget { decoration: BoxDecoration( color: _kBgSurface, borderRadius: BorderRadius.circular(16), - border: Border.all(color: _kCyan.withOpacity(0.15), width: 1), + border: Border.all(color: _kCyan.withValues(alpha: 0.15), width: 1), ), child: Row( children: [ @@ -255,12 +252,12 @@ class MapMenuWidget extends StatelessWidget { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - _kCyan.withOpacity(0.2), - _kAmber.withOpacity(0.12), + _kCyan.withValues(alpha: 0.2), + _kAmber.withValues(alpha: 0.12), ], ), border: - Border.all(color: _kCyan.withOpacity(0.35), width: 1.5), + Border.all(color: _kCyan.withValues(alpha: 0.35), width: 1.5), ), child: Icon(Icons.person_rounded, color: _kCyan, size: 28), ), @@ -277,7 +274,7 @@ class MapMenuWidget extends StatelessWidget { border: Border.all(color: _kBg, width: 2), boxShadow: [ BoxShadow( - color: const Color(0xFF00E676).withOpacity(0.5), + color: const Color(0xFF00E676).withValues(alpha: 0.5), blurRadius: 6, ), ], @@ -365,7 +362,7 @@ class MapMenuWidget extends StatelessWidget { gradient: LinearGradient( colors: [ Colors.transparent, - _kCyan.withOpacity(0.15), + _kCyan.withValues(alpha: 0.15), Colors.transparent, ], ), @@ -419,7 +416,7 @@ class _QuickBtn extends StatelessWidget { decoration: BoxDecoration( color: _kBgSurface, borderRadius: BorderRadius.circular(12), - border: Border.all(color: _kCyan.withOpacity(0.12), width: 1), + border: Border.all(color: _kCyan.withValues(alpha: 0.12), width: 1), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -463,7 +460,7 @@ class MenuListItem extends StatelessWidget { Widget build(BuildContext context) { final iconColor = isDestructive ? const Color(0xFFFF5252) - : (color ?? _kCyan.withOpacity(0.80)); + : (color ?? _kCyan.withValues(alpha: 0.80)); final textColor = isDestructive ? const Color(0xFFFF5252) : (color ?? _kText); @@ -472,8 +469,8 @@ class MenuListItem extends StatelessWidget { child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), - splashColor: _kCyan.withOpacity(0.07), - highlightColor: _kCyan.withOpacity(0.04), + splashColor: _kCyan.withValues(alpha: 0.07), + highlightColor: _kCyan.withValues(alpha: 0.04), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( @@ -484,8 +481,8 @@ class MenuListItem extends StatelessWidget { height: 36, decoration: BoxDecoration( color: isDestructive - ? const Color(0xFFFF5252).withOpacity(0.08) - : _kCyan.withOpacity(0.07), + ? const Color(0xFFFF5252).withValues(alpha: 0.08) + : _kCyan.withValues(alpha: 0.07), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, size: 19, color: iconColor), @@ -504,7 +501,7 @@ class MenuListItem extends StatelessWidget { ), Icon( Icons.chevron_right_rounded, - color: _kTextMuted.withOpacity(0.4), + color: _kTextMuted.withValues(alpha: 0.4), size: 18, ), ], @@ -520,7 +517,7 @@ class _MenuGridPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() - ..color = AppColor.cyanBlue.withOpacity(0.04) + ..color = AppColor.cyanBlue.withValues(alpha: 0.04) ..strokeWidth = 0.5; const spacing = 36.0; for (double y = 0; y < size.height; y += spacing) { diff --git a/lib/views/home/map_widget.dart/menu_map_page.dart b/lib/views/home/map_widget.dart/menu_map_page.dart index 9d8cbe6..b600c8c 100644 --- a/lib/views/home/map_widget.dart/menu_map_page.dart +++ b/lib/views/home/map_widget.dart/menu_map_page.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import '../../../constant/box_name.dart'; import '../../../constant/colors.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/map_engine_controller.dart'; import '../../../main.dart'; class MenuIconMapPageWidget extends StatelessWidget { @@ -13,7 +13,7 @@ class MenuIconMapPageWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return GetBuilder( + return GetBuilder( builder: (controller) => Positioned( top: Get.height * .008, left: box.read(BoxName.lang) != 'ar' ? 5 : null, diff --git a/lib/views/home/map_widget.dart/passengerRideLoctionWidget.dart b/lib/views/home/map_widget.dart/passengerRideLoctionWidget.dart index 47c3a1c..ebdfbc8 100644 --- a/lib/views/home/map_widget.dart/passengerRideLoctionWidget.dart +++ b/lib/views/home/map_widget.dart/passengerRideLoctionWidget.dart @@ -4,7 +4,7 @@ import 'dart:ui'; // مهم لإضافة تأثير الضبابية import '../../../constant/colors.dart'; import '../../../constant/style.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/location_search_controller.dart'; // --- الويدجت الرئيسية بالتصميم الجديد --- class PassengerRideLocationWidget extends StatefulWidget { @@ -43,7 +43,7 @@ class _PassengerRideLocationWidgetState @override Widget build(BuildContext context) { - return GetBuilder(builder: (controller) { + return GetBuilder(builder: (controller) { // --- نفس شرط الإظهار الخاص بك --- return AnimatedPositioned( duration: const Duration(milliseconds: 300), @@ -60,9 +60,9 @@ class _PassengerRideLocationWidgetState child: Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), decoration: BoxDecoration( - color: AppColor.secondaryColor.withOpacity(0.85), + color: AppColor.secondaryColor.withValues(alpha: 0.85), borderRadius: BorderRadius.circular(50.0), - border: Border.all(color: AppColor.writeColor.withOpacity(0.2)), + border: Border.all(color: AppColor.writeColor.withValues(alpha: 0.2)), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -89,7 +89,7 @@ class _PassengerRideLocationWidgetState Text( "Move the map to adjust the pin".tr, style: AppStyle.subtitle.copyWith( - color: AppColor.writeColor.withOpacity(0.7), + color: AppColor.writeColor.withValues(alpha: 0.7), ), ), ], diff --git a/lib/views/home/map_widget.dart/payment_method.page.dart b/lib/views/home/map_widget.dart/payment_method.page.dart index 3d5ca80..fa79a1d 100644 --- a/lib/views/home/map_widget.dart/payment_method.page.dart +++ b/lib/views/home/map_widget.dart/payment_method.page.dart @@ -8,8 +8,7 @@ import 'package:Intaleq/views/widgets/elevated_btn.dart'; import '../../../constant/colors.dart'; import '../../../constant/style.dart'; -import '../../../controller/functions/digit_obsecur_formate.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/map_engine_controller.dart'; class PaymentMethodPage extends StatelessWidget { const PaymentMethodPage({ @@ -18,7 +17,7 @@ class PaymentMethodPage extends StatelessWidget { @override Widget build(BuildContext context) { - return GetBuilder( + return GetBuilder( builder: (controller) => Positioned( right: 5, bottom: 5, diff --git a/lib/views/home/map_widget.dart/picker_animation_container.dart b/lib/views/home/map_widget.dart/picker_animation_container.dart index 698c67d..410dbb4 100644 --- a/lib/views/home/map_widget.dart/picker_animation_container.dart +++ b/lib/views/home/map_widget.dart/picker_animation_container.dart @@ -4,20 +4,23 @@ import 'package:Intaleq/constant/table_names.dart'; import '../../../constant/colors.dart'; import '../../../constant/style.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/location_search_controller.dart'; +import '../../../controller/home/map/map_engine_controller.dart'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; import '../../../main.dart'; import '../../widgets/elevated_btn.dart'; import 'form_search_places_destenation.dart'; class PickerAnimtionContainerFormPlaces extends StatelessWidget { - PickerAnimtionContainerFormPlaces({ - super.key, - }); - final controller = MapPassengerController(); + const PickerAnimtionContainerFormPlaces({super.key}); + @override Widget build(BuildContext context) { - // DbSql sql = DbSql.instance; - return GetBuilder( + final mapEngine = Get.find(); + final locationSearch = Get.find(); + final rideLifecycle = Get.find(); + + return GetBuilder( builder: (controller) => Positioned( bottom: 0, left: 0, @@ -101,8 +104,7 @@ class PickerAnimtionContainerFormPlaces extends StatelessWidget { ? Center( child: Column( mainAxisAlignment: - MainAxisAlignment - .center, + MainAxisAlignment.center, children: [ const Icon( Icons @@ -132,9 +134,9 @@ class PickerAnimtionContainerFormPlaces extends StatelessWidget { children: [ TextButton( onPressed: () async { - await controller + await rideLifecycle .getDirectionMap( - '${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}', + '${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}', '${favoritePlaces[index]['latitude']},${favoritePlaces[index]['longitude']}', ); controller @@ -143,7 +145,7 @@ class PickerAnimtionContainerFormPlaces extends StatelessWidget { .changeBottomSheetShown( forceValue: true); - controller + rideLifecycle .bottomSheet(); Get.back(); }, @@ -189,24 +191,22 @@ class PickerAnimtionContainerFormPlaces extends StatelessWidget { ], ), if (controller.isPickerShown && - controller.placesDestination.isEmpty) + locationSearch.placesDestination.isEmpty) MyElevatedButton( title: 'Go to this Target'.tr, onPressed: () async { - await controller.getDirectionMap( - '${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}', - '${controller.newMyLocation.latitude},${controller.newMyLocation.longitude}', + await rideLifecycle.getDirectionMap( + '${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}', + '${locationSearch.newMyLocation.latitude},${locationSearch.newMyLocation.longitude}', ); controller.changePickerShown(); controller.changeBottomSheetShown( forceValue: true); - controller.bottomSheet(); - // await sql - // .getAllData(TableName.placesFavorite) + rideLifecycle.bottomSheet(); }, ), if (controller.isPickerShown && - controller.placesDestination.isEmpty) + locationSearch.placesDestination.isEmpty) const SizedBox(), ], ), diff --git a/lib/views/home/map_widget.dart/points_page_for_rider.dart b/lib/views/home/map_widget.dart/points_page_for_rider.dart index 08c7eaf..761fe69 100644 --- a/lib/views/home/map_widget.dart/points_page_for_rider.dart +++ b/lib/views/home/map_widget.dart/points_page_for_rider.dart @@ -3,21 +3,25 @@ import 'package:get/get.dart'; import 'package:Intaleq/constant/style.dart'; import '../../../constant/colors.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/location_search_controller.dart'; +import '../../../controller/home/map/map_engine_controller.dart'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; import '../../../controller/home/points_for_rider_controller.dart'; class PointsPageForRider extends StatelessWidget { PointsPageForRider({ super.key, }); - MapPassengerController mapPassengerController = - Get.put(MapPassengerController()); + + final locationSearch = Get.find(); + final mapEngine = Get.find(); + final rideLifecycle = Get.find(); @override Widget build(BuildContext context) { - Get.put(WayPointController()); + Get.find(); - return GetBuilder(builder: (controller) { + return GetBuilder(builder: (controller) { return Positioned( bottom: 2, left: 2, @@ -34,7 +38,7 @@ class PointsPageForRider extends StatelessWidget { children: [ IconButton( onPressed: () { - mapPassengerController.downPoints(); + mapEngine.downPoints(); }, icon: const Icon(Icons.arrow_drop_down_circle_outlined), ), @@ -52,7 +56,7 @@ class PointsPageForRider extends StatelessWidget { wayPointController.wayPoints.length > 1 ? ElevatedButton( onPressed: () async { - mapPassengerController + locationSearch .getMapPointsForAllMethods(); }, child: const Text('Get Direction'), @@ -74,7 +78,6 @@ class PointsPageForRider extends StatelessWidget { .entries .map((entry) { final index = entry.key; - final wayPoint = entry.value; return Padding( key: ValueKey(index), padding: const EdgeInsets.all(1), @@ -98,7 +101,7 @@ class PointsPageForRider extends StatelessWidget { content: SizedBox( width: Get.width, height: 400, - child: mapPassengerController + child: locationSearch .placeListResponse[index]), ); }, @@ -106,13 +109,13 @@ class PointsPageForRider extends StatelessWidget { decoration: BoxDecoration( border: Border.all(), color: - AppColor.accentColor.withOpacity(.5)), + AppColor.accentColor.withValues(alpha: 0.5)), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(index > 0 - ? mapPassengerController + ? locationSearch .currentLocationStringAll[index] .toString() : ''), @@ -239,7 +242,6 @@ class PointsPageForRider extends StatelessWidget { } void showAddLocationDialog(BuildContext context, int index) { - final TextEditingController locationController = TextEditingController(); // Get.put(WayPointController()); showDialog( context: context, @@ -298,26 +300,24 @@ class AppBarPointsPageForRider extends StatelessWidget { color: AppColor.primaryColor, ), ), - Container( - child: Row( - children: [ - CircleAvatar( - backgroundColor: AppColor.primaryColor, - maxRadius: 15, - child: Icon( - Icons.person, - color: AppColor.secondaryColor, - ), + Row( + children: [ + CircleAvatar( + backgroundColor: AppColor.primaryColor, + maxRadius: 15, + child: Icon( + Icons.person, + color: AppColor.secondaryColor, ), - TextButton( - onPressed: () {}, - child: Text( - "Switch Rider".tr, - style: AppStyle.title, - ), + ), + TextButton( + onPressed: () {}, + child: Text( + "Switch Rider".tr, + style: AppStyle.title, ), - ], - ), + ), + ], ), Icon( Icons.clear, diff --git a/lib/views/home/map_widget.dart/ride_begin_passenger.dart b/lib/views/home/map_widget.dart/ride_begin_passenger.dart index 2a45f5b..1a1bd0d 100644 --- a/lib/views/home/map_widget.dart/ride_begin_passenger.dart +++ b/lib/views/home/map_widget.dart/ride_begin_passenger.dart @@ -11,7 +11,9 @@ 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'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; +import '../../../controller/home/map/ui_interactions_controller.dart'; +import '../../../controller/home/map/ride_state.dart'; import '../../../controller/profile/profile_controller.dart'; import '../../../main.dart'; import '../../../views/home/profile/complaint_page.dart'; @@ -24,9 +26,10 @@ class RideBeginPassenger extends StatelessWidget { final ProfileController profileController = Get.put(ProfileController()); final AudioRecorderController audioController = Get.put(AudioRecorderController()); + final uiController = Get.find(); return Obx(() { - final controller = Get.find(); + final controller = Get.find(); // شرط الإظهار final bool isVisible = @@ -50,8 +53,8 @@ class RideBeginPassenger extends StatelessWidget { boxShadow: [ BoxShadow( color: Get.isDarkMode - ? Colors.black.withOpacity(0.4) - : Colors.black.withOpacity(0.1), + ? Colors.black.withValues(alpha: 0.4) + : Colors.black.withValues(alpha: 0.1), blurRadius: 20, spreadRadius: 2, offset: const Offset(0, -3), @@ -69,7 +72,7 @@ class RideBeginPassenger extends StatelessWidget { width: 40, height: 4, decoration: BoxDecoration( - color: AppColor.grayColor.withOpacity(0.3), + color: AppColor.grayColor.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(10), ), ), @@ -85,7 +88,7 @@ class RideBeginPassenger extends StatelessWidget { Divider( height: 1, thickness: 0.5, - color: AppColor.grayColor.withOpacity(0.2)), + color: AppColor.grayColor.withValues(alpha: 0.2)), const SizedBox(height: 12), @@ -104,7 +107,7 @@ class RideBeginPassenger extends StatelessWidget { } // --- الهيدر (بدون تغيير، ممتاز) --- - Widget _buildCompactHeader(MapPassengerController controller) { + Widget _buildCompactHeader(RideLifecycleController controller) { return Row( children: [ // صورة السائق @@ -112,7 +115,7 @@ class RideBeginPassenger extends StatelessWidget { decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: AppColor.primaryColor.withOpacity(0.5), width: 1.5), + color: AppColor.primaryColor.withValues(alpha: 0.5), width: 1.5), ), child: CircleAvatar( radius: 24, @@ -166,9 +169,9 @@ class RideBeginPassenger extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( - color: AppColor.writeColor.withOpacity(0.05), + color: AppColor.writeColor.withValues(alpha: 0.05), border: Border.all( - color: AppColor.grayColor.withOpacity(0.2)), + color: AppColor.grayColor.withValues(alpha: 0.2)), borderRadius: BorderRadius.circular(4), ), child: Text( @@ -190,7 +193,7 @@ class RideBeginPassenger extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( - color: AppColor.primaryColor.withOpacity(0.08), + color: AppColor.primaryColor.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(8), ), child: Column( @@ -216,9 +219,10 @@ class RideBeginPassenger extends StatelessWidget { // --- الأزرار (بدون تغيير) --- Widget _buildCompactActionButtons( BuildContext context, - MapPassengerController controller, + RideLifecycleController controller, ProfileController profileController, AudioRecorderController audioController) { + final uiController = Get.find(); return SizedBox( height: 60, child: Row( @@ -228,7 +232,7 @@ class RideBeginPassenger extends StatelessWidget { icon: Icons.sos_rounded, label: 'SOS'.tr, color: AppColor.redColor, - bgColor: AppColor.redColor.withOpacity(0.1), + bgColor: AppColor.redColor.withValues(alpha: 0.1), onTap: () async { if (box.read(BoxName.sosPhonePassenger) == null) { await profileController.updatField( @@ -244,24 +248,26 @@ class RideBeginPassenger extends StatelessWidget { icon: FontAwesome.whatsapp, label: 'WhatsApp'.tr, color: const Color(0xFF25D366), - bgColor: const Color(0xFF25D366).withOpacity(0.1), + bgColor: const Color(0xFF25D366).withValues(alpha: 0.1), onTap: () async { - if (box.read(BoxName.sosPhonePassenger) == null) { - await profileController.updatField( - 'sosPhone', TextInputType.phone); + final phone = box.read(BoxName.sosPhonePassenger); + if (phone == null || phone.toString().isEmpty) { + // لا يوجد رقم طوارئ — نعرض الديالوج لإدخاله + await uiController.shareTripWithFamily(); } else { - final phone = controller.formatSyrianPhoneNumber( - box.read(BoxName.sosPhonePassenger).toString()); - controller.sendWhatsapp(phone); + final formattedPhone = uiController.formatSyrianPhoneNumber( + phone.toString()); + uiController.sendWhatsapp(formattedPhone); } }, ), + _compactBtn( icon: Icons.share, label: 'Share'.tr, color: AppColor.primaryColor, - bgColor: AppColor.primaryColor.withOpacity(0.1), - onTap: () async => await controller.shareTripWithFamily(), + bgColor: AppColor.primaryColor.withValues(alpha: 0.1), + onTap: () async => await uiController.shareTripWithFamily(), ), GetBuilder( init: audioController, @@ -275,15 +281,19 @@ class RideBeginPassenger extends StatelessWidget { ? AppColor.redColor : AppColor.primaryColor, bgColor: audioCtx.isRecording - ? AppColor.redColor.withOpacity(0.1) - : AppColor.primaryColor.withOpacity(0.1), + ? AppColor.redColor.withValues(alpha: 0.1) + : AppColor.primaryColor.withValues(alpha: 0.1), onTap: () async { if (!audioCtx.isRecording) { - await audioCtx.startRecording(); - Toast.show(context, 'Start Record'.tr, AppColor.greenColor); + await audioCtx.startRecording(rideId: controller.rideId); + if (context.mounted) { + Toast.show(context, 'Start Record'.tr, AppColor.greenColor); + } } else { await audioCtx.stopRecording(); - Toast.show(context, 'Record saved'.tr, AppColor.greenColor); + if (context.mounted) { + Toast.show(context, 'Record saved'.tr, AppColor.greenColor); + } } }, ); @@ -293,7 +303,7 @@ class RideBeginPassenger extends StatelessWidget { icon: Icons.info_outline_rounded, label: 'Report'.tr, color: AppColor.grayColor, - bgColor: AppColor.writeColor.withOpacity(0.1), + bgColor: AppColor.writeColor.withValues(alpha: 0.1), onTap: () => Get.to(() => ComplaintPage()), ), ], diff --git a/lib/views/home/map_widget.dart/ride_from_start_app.dart b/lib/views/home/map_widget.dart/ride_from_start_app.dart index 7654e24..9beeb74 100644 --- a/lib/views/home/map_widget.dart/ride_from_start_app.dart +++ b/lib/views/home/map_widget.dart/ride_from_start_app.dart @@ -8,7 +8,9 @@ import '../../../constant/box_name.dart'; import '../../../constant/colors.dart'; import '../../../constant/links.dart'; import '../../../constant/style.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; +import '../../../controller/home/map/ui_interactions_controller.dart'; +import '../../../controller/home/map/ride_state.dart'; import '../../../controller/profile/profile_controller.dart'; import '../../../main.dart'; @@ -18,8 +20,10 @@ class RideFromStartApp extends StatelessWidget { @override Widget build(BuildContext context) { final profileController = Get.put(ProfileController()); - final MapPassengerController controller = - Get.find(); + final RideLifecycleController controller = + Get.find(); + final UiInteractionsController uiController = + Get.find(); return Obx(() { final bool isRideActive = @@ -59,7 +63,7 @@ class RideFromStartApp extends StatelessWidget { boxShadow: [ BoxShadow( color: Get.isDarkMode - ? Colors.black.withOpacity(0.4) + ? Colors.black.withValues(alpha: 0.4) : Colors.black12, blurRadius: 15.0, spreadRadius: 5.0, @@ -78,7 +82,7 @@ class RideFromStartApp extends StatelessWidget { height: 4, margin: const EdgeInsets.only(bottom: 15), decoration: BoxDecoration( - color: AppColor.grayColor.withOpacity(0.3), + color: AppColor.grayColor.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(10), ), ), @@ -134,7 +138,7 @@ class RideFromStartApp extends StatelessWidget { Container( width: 1, height: 12, - color: AppColor.grayColor.withOpacity(0.3)), + color: AppColor.grayColor.withValues(alpha: 0.3)), const SizedBox(width: 8), Text( "$carType - $carModel", @@ -160,7 +164,7 @@ class RideFromStartApp extends StatelessWidget { const EdgeInsets.symmetric(vertical: 12, horizontal: 10), decoration: BoxDecoration( color: AppColor.grayColor - .withOpacity(0.1), // خلفية رمادية خفيفة جداً + .withValues(alpha: 0.1), // خلفية رمادية خفيفة جداً borderRadius: BorderRadius.circular(15), ), child: Row( @@ -188,7 +192,7 @@ class RideFromStartApp extends StatelessWidget { flex: 2, child: ElevatedButton.icon( onPressed: () => _checkAndCall( - controller.sendWhatsapp, profileController), + uiController.sendWhatsapp, profileController), icon: const Icon(FontAwesome.whatsapp, color: Colors.white), label: Text("Share Trip".tr, @@ -252,9 +256,9 @@ class RideFromStartApp extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( - color: color.withOpacity(0.1), + color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), - border: Border.all(color: color.withOpacity(0.5)), + border: Border.all(color: color.withValues(alpha: 0.5)), ), child: Text( text, @@ -297,7 +301,7 @@ class RideFromStartApp extends StatelessWidget { return Container( height: 30, width: 1, - color: AppColor.grayColor.withOpacity(0.2), + color: AppColor.grayColor.withValues(alpha: 0.2), ); } diff --git a/lib/views/home/map_widget.dart/searching_captain_window.dart b/lib/views/home/map_widget.dart/searching_captain_window.dart index 4f8472b..cd8fc44 100644 --- a/lib/views/home/map_widget.dart/searching_captain_window.dart +++ b/lib/views/home/map_widget.dart/searching_captain_window.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:Intaleq/constant/colors.dart'; import 'package:Intaleq/constant/style.dart'; -import 'package:Intaleq/controller/home/map_passenger_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_state.dart'; // --- الويدجت الرئيسية بالتصميم الجديد --- class SearchingCaptainWindow extends StatefulWidget { @@ -36,7 +37,7 @@ class _SearchingCaptainWindowState extends State // [تعديل 1] نستخدم Obx للاستماع إلى التغييرات في حالة الرحلة return Obx(() { // ابحث عن الكنترولر مرة واحدة - final controller = Get.find(); + final controller = Get.find(); // [تعديل 2] شرط الإظهار يعتمد الآن على حالة الرحلة مباشرة final bool isVisible = @@ -58,7 +59,7 @@ class _SearchingCaptainWindowState extends State ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.2), + color: Colors.black.withValues(alpha: 0.2), blurRadius: 20, offset: const Offset(0, -5), ), @@ -83,7 +84,7 @@ class _SearchingCaptainWindowState extends State style: OutlinedButton.styleFrom( foregroundColor: AppColor.writeColor, side: - BorderSide(color: AppColor.writeColor.withOpacity(0.3)), + BorderSide(color: AppColor.writeColor.withValues(alpha: 0.3)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 12), @@ -99,7 +100,7 @@ class _SearchingCaptainWindowState extends State } // --- ويدجت بناء أنيميشن الرادار --- - Widget _buildRadarAnimation(MapPassengerController controller) { + Widget _buildRadarAnimation(RideLifecycleController controller) { return SizedBox( height: 180, // ارتفاع ثابت لمنطقة الأنيميشن child: Stack( @@ -125,7 +126,7 @@ class _SearchingCaptainWindowState extends State decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( - color: AppColor.primaryColor.withOpacity(0.7), + color: AppColor.primaryColor.withValues(alpha: 0.7), width: 2, ), ), @@ -147,7 +148,7 @@ class _SearchingCaptainWindowState extends State Text( 'Searching for the nearest captain...'.tr, style: AppStyle.subtitle - .copyWith(color: AppColor.writeColor.withOpacity(0.7)), + .copyWith(color: AppColor.writeColor.withValues(alpha: 0.7)), textAlign: TextAlign.center, ), const SizedBox(height: 16), @@ -166,12 +167,12 @@ class _SearchingCaptainWindowState extends State CircularProgressIndicator( strokeWidth: 3, color: AppColor.primaryColor, - backgroundColor: AppColor.primaryColor.withOpacity(0.2), + backgroundColor: AppColor.primaryColor.withValues(alpha: 0.2), ), Center( child: Icon( Icons.search, - color: AppColor.writeColor.withOpacity(0.8), + color: AppColor.writeColor.withValues(alpha: 0.8), ), ), ], diff --git a/lib/views/home/map_widget.dart/select_driver_mishwari.dart b/lib/views/home/map_widget.dart/select_driver_mishwari.dart index 2de0a7d..f20083d 100644 --- a/lib/views/home/map_widget.dart/select_driver_mishwari.dart +++ b/lib/views/home/map_widget.dart/select_driver_mishwari.dart @@ -1,6 +1,6 @@ import 'package:Intaleq/constant/colors.dart'; import 'package:Intaleq/constant/style.dart'; -import 'package:Intaleq/controller/home/map_passenger_controller.dart'; +import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart'; import 'package:Intaleq/views/widgets/elevated_btn.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -10,8 +10,10 @@ import '../../../constant/links.dart'; import '../../../print.dart'; class CupertinoDriverListWidget extends StatelessWidget { - MapPassengerController mapPassengerController = - Get.put(MapPassengerController()); + CupertinoDriverListWidget({super.key}); + + final RideLifecycleController mapPassengerController = + Get.find(); @override Widget build(BuildContext context) { return CupertinoPageScaffold( @@ -158,7 +160,7 @@ class CupertinoDriverListWidget extends StatelessWidget { ), onTap: () { Log.print(' driver["id"]: ${driver['driver_id']}'); - Get.find().driverIdVip = + Get.find().driverIdVip = driver['driver_id']; // Handle driver selection @@ -266,8 +268,6 @@ class CupertinoDriverListWidget extends StatelessWidget { } void showDateTimePickerDialog(Map driver) { - DateTime selectedDateTime = DateTime.now(); - Get.defaultDialog( barrierDismissible: false, title: "Select date and time of trip".tr, @@ -302,7 +302,9 @@ class CupertinoDriverListWidget extends StatelessWidget { } class DateTimePickerWidget extends StatelessWidget { - final MapPassengerController controller = Get.put(MapPassengerController()); + DateTimePickerWidget({super.key}); + + final RideLifecycleController controller = Get.find(); @override Widget build(BuildContext context) { diff --git a/lib/views/home/map_widget.dart/timer_for_cancell_trip_from_passenger.dart b/lib/views/home/map_widget.dart/timer_for_cancell_trip_from_passenger.dart index ed0c2d0..9282d38 100644 --- a/lib/views/home/map_widget.dart/timer_for_cancell_trip_from_passenger.dart +++ b/lib/views/home/map_widget.dart/timer_for_cancell_trip_from_passenger.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../../constant/style.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; -GetBuilder timerForCancelTripFromPassenger() { - return GetBuilder( +GetBuilder timerForCancelTripFromPassenger() { + return GetBuilder( builder: (controller) { final isNearEnd = controller.remainingTime <= 5; // Define a threshold for "near end" diff --git a/lib/views/home/map_widget.dart/timer_to_passenger_from_driver.dart b/lib/views/home/map_widget.dart/timer_to_passenger_from_driver.dart index 0077fac..d11f005 100644 --- a/lib/views/home/map_widget.dart/timer_to_passenger_from_driver.dart +++ b/lib/views/home/map_widget.dart/timer_to_passenger_from_driver.dart @@ -4,7 +4,7 @@ import 'package:Intaleq/views/widgets/elevated_btn.dart'; import '../../../constant/colors.dart'; import '../../../constant/style.dart'; -import '../../../controller/home/map_passenger_controller.dart'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; import 'ride_begin_passenger.dart'; class TimerToPassengerFromDriver extends StatelessWidget { @@ -14,7 +14,7 @@ class TimerToPassengerFromDriver extends StatelessWidget { @override Widget build(BuildContext context) { - return GetBuilder(builder: (controller) { + return GetBuilder(builder: (controller) { if (controller.remainingTime == 0 && (controller.isDriverInPassengerWay == true || controller.timeToPassengerFromDriverAfterApplied > 0)) { diff --git a/lib/views/home/map_widget.dart/vip_begin.dart b/lib/views/home/map_widget.dart/vip_begin.dart index c1f0f3f..0befcab 100644 --- a/lib/views/home/map_widget.dart/vip_begin.dart +++ b/lib/views/home/map_widget.dart/vip_begin.dart @@ -12,7 +12,8 @@ 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'; +import '../../../controller/home/map/ride_lifecycle_controller.dart'; +import '../../../controller/home/map/ui_interactions_controller.dart'; class VipRideBeginPassenger extends StatelessWidget { const VipRideBeginPassenger({ @@ -24,8 +25,8 @@ class VipRideBeginPassenger extends StatelessWidget { ProfileController profileController = Get.put(ProfileController()); AudioRecorderController audioController = Get.put(AudioRecorderController()); - // Get.put(MapPassengerController()); - return GetBuilder(builder: (controller) { + final uiController = Get.find(); + return GetBuilder(builder: (controller) { if (controller.statusRideVip == 'Begin' || !controller.statusRideFromStart) { return Positioned( @@ -148,9 +149,11 @@ class VipRideBeginPassenger extends StatelessWidget { child: audioController.isRecording == false ? IconButton( onPressed: () async { - await audioController.startRecording(); - Toast.show(context, 'Start Record'.tr, - AppColor.greenColor); + await audioController.startRecording(rideId: controller.rideId); + if (context.mounted) { + Toast.show(context, 'Start Record'.tr, + AppColor.greenColor); + } }, icon: const Icon( Icons.play_circle_fill_outlined, @@ -162,8 +165,10 @@ class VipRideBeginPassenger extends StatelessWidget { : IconButton( onPressed: () async { await audioController.stopRecording(); - Toast.show(context, 'Record saved'.tr, - AppColor.greenColor); + if (context.mounted) { + Toast.show(context, 'Record saved'.tr, + AppColor.greenColor); + } }, icon: const Icon( Icons.stop_circle, @@ -215,15 +220,11 @@ class VipRideBeginPassenger extends StatelessWidget { profileController.prfoileData['sosPhone']); } } else { - String phoneNumber = box - .read(BoxName.sosPhonePassenger) - .toString(); - // phoneNumber = phoneNumber.replaceAll('0', ''); var phone = box.read(BoxName.countryCode) == 'Egypt' ? '+2${box.read(BoxName.sosPhonePassenger)}' : '+962${box.read(BoxName.sosPhonePassenger)}'; - controller.sendWhatsapp(phone); + uiController.sendWhatsapp(phone); } }, icon: const Icon( @@ -237,7 +238,7 @@ class VipRideBeginPassenger extends StatelessWidget { width: Get.width * .15, child: IconButton( onPressed: () async { - await controller.shareTripWithFamily(); + await uiController.shareTripWithFamily(); }, icon: const Icon( AntDesign.Safety, @@ -283,13 +284,12 @@ class VipRideBeginPassenger extends StatelessWidget { } class StreamCounter extends StatelessWidget { - const StreamCounter({Key? key}) : super(key: key); + const StreamCounter({super.key}); @override // Build the UI based on the timer value Widget build(BuildContext context) { - Get.put(MapPassengerController()); - return GetBuilder(builder: (controller) { + return GetBuilder(builder: (controller) { return StreamBuilder( initialData: 0, stream: controller.timerController.stream, diff --git a/lib/views/home/navigation/navigation_controller.dart b/lib/views/home/navigation/navigation_controller.dart index a6927c7..92c5513 100644 --- a/lib/views/home/navigation/navigation_controller.dart +++ b/lib/views/home/navigation/navigation_controller.dart @@ -268,9 +268,6 @@ class NavigationController extends GetxController }, ]; - static final String _routeApiBaseUrl = - "${AppLink.routesOsm}/route/v1/driving"; - IconData get currentManeuverIcon { switch (currentManeuverModifier) { case 4: // Arrive @@ -378,7 +375,6 @@ class NavigationController extends GetxController void onMapCreated(IntaleqMapController controller) async { Log.print("DEBUG: NavigationController.onMapCreated called"); mapController = controller; - await onStyleLoaded(); } Future onStyleLoaded() async { @@ -577,7 +573,7 @@ class NavigationController extends GetxController } void _checkOffRoute(LatLng pos) { - if (_autoRecalcInProgress || isLoading) return; + if (!isNavigating || _autoRecalcInProgress || isLoading) return; if (_fullRouteCoordinates.isEmpty) return; const int searchWindow = 80; @@ -604,7 +600,6 @@ class NavigationController extends GetxController if (elapsed >= _offRouteTriggerSeconds) { _offRouteStartTime = null; _autoRecalcInProgress = true; - // Smart reroute: check if we have alternative routes available _smartRecalculateRoute(pos); } } @@ -613,40 +608,11 @@ class NavigationController extends GetxController } } - /// الحل الذكي: إذا كان هناك مسارات بديلة متاحة، اختر الأقرب. - /// وإلا فاطلب مسار جديد من الموقع الحالي إلى الوجهة. + /// Recalculate immediately from the latest GPS point to the destination. Future _smartRecalculateRoute(LatLng currentPos) async { try { - // Check if we have alternative routes - if (routes.isNotEmpty && selectedRouteIndex < routes.length - 1) { - // Try using the next alternative route - final nextIndex = selectedRouteIndex + 1; - final nextRoute = routes[nextIndex]; - - // Calculate distance from current position to this alternative route's start - double minDist = double.infinity; - for (var coord in nextRoute.coordinates) { - final d = Geolocator.distanceBetween( - currentPos.latitude, - currentPos.longitude, - coord.latitude, - coord.longitude, - ); - if (d < minDist) minDist = d; - } - - // If this alternative is reasonable, switch to it - if (minDist < 100) { - selectRoute(nextIndex); - Log.print("DEBUG: Switched to alternative route due to deviation"); - _autoRecalcInProgress = false; - return; - } - } - - // No good alternative, recalculate from current position to destination if (_finalDestination != null) { - await recalculateRoute(); + await recalculateRoute(origin: currentPos, keepNavigationActive: true); } _autoRecalcInProgress = false; } catch (e) { @@ -906,7 +872,8 @@ class NavigationController extends GetxController return const LatLng(31.7225, 35.9933); // Queen Alia Airport (JO) } - Future getRoute(LatLng origin, LatLng destination) async { + Future getRoute(LatLng origin, LatLng destination, + {bool keepNavigationActive = false}) async { isLoading = true; update(); @@ -1007,9 +974,8 @@ class NavigationController extends GetxController currentStepIndex = 0; _nextInstructionSpoken = false; - // Don't start navigating immediately, wait for user to press Start - isNavigating = false; - _cameraLockedToUser = false; + isNavigating = keepNavigationActive; + _cameraLockedToUser = keepNavigationActive; _offRouteStartTime = null; isLoading = false; @@ -1032,7 +998,10 @@ class NavigationController extends GetxController // Re-add car marker after polyline updates (ensures it stays on top) if (isStyleLoaded) _updateCarMarker(); - if (_fullRouteCoordinates.length >= 2) { + if (keepNavigationActive && myLocation != null) { + animateCameraToPosition(myLocation!, + bearing: _smoothedHeading, zoom: _targetZoom, tilt: _targetTilt); + } else if (_fullRouteCoordinates.length >= 2) { final bounds = data['bbox'] != null && (data['bbox'] as List).length == 4 ? LatLngBounds( @@ -1117,12 +1086,23 @@ class NavigationController extends GetxController } } - Future recalculateRoute() async { - if (myLocation == null || _finalDestination == null || isLoading) return; + Future recalculateRoute( + {LatLng? origin, bool keepNavigationActive = false}) async { + final LatLng? routeOrigin = origin ?? myLocation; + if (routeOrigin == null || _finalDestination == null || isLoading) return; + isLoading = true; update(); - mySnackbarInfo('جاري حساب مسار جديد...'); - await getRoute(myLocation!, _finalDestination!); + + markers = markers.where((m) => m.markerId.value != 'origin').toSet(); + markers.add(Marker( + markerId: const MarkerId('origin'), + position: routeOrigin, + icon: InlqBitmap.fromStyleImage('start_icon'), + )); + + await getRoute(routeOrigin, _finalDestination!, + keepNavigationActive: keepNavigationActive); isLoading = false; update(); } @@ -1290,17 +1270,19 @@ class NavigationController extends GetxController try { // ✅ Use searchPlaces from intaleq_maps SDK final results = await mapController!.searchPlaces(q); - + if (myLocation != null) { for (final p in results) { final plat = double.tryParse(p['latitude']?.toString() ?? '0') ?? 0.0; - final plng = double.tryParse(p['longitude']?.toString() ?? '0') ?? 0.0; - p['distanceKm'] = _haversineKm(myLocation!.latitude, myLocation!.longitude, plat, plng); + final plng = + double.tryParse(p['longitude']?.toString() ?? '0') ?? 0.0; + p['distanceKm'] = _haversineKm( + myLocation!.latitude, myLocation!.longitude, plat, plng); } results.sort((a, b) => (a['distanceKm'] as double).compareTo(b['distanceKm'] as double)); } - + placesDestination = results; update(); } catch (e) { diff --git a/lib/views/home/navigation/navigation_view.dart b/lib/views/home/navigation/navigation_view.dart index c654484..5576243 100644 --- a/lib/views/home/navigation/navigation_view.dart +++ b/lib/views/home/navigation/navigation_view.dart @@ -41,6 +41,7 @@ class NavigationView extends StatelessWidget { IntaleqMap( apiKey: Env.mapSaasKey, onMapCreated: c.onMapCreated, + onStyleLoaded: c.onStyleLoaded, onLongPress: (pos) => c.onMapLongPressed(Point(0, 0), pos), onTap: (pos) => c.onMapTapped(Point(0, 0), pos), markers: c.markers, diff --git a/lib/views/home/navigation/navigation_view_old.dart b/lib/views/home/navigation/navigation_view_old.dart deleted file mode 100644 index add5b39..0000000 --- a/lib/views/home/navigation/navigation_view_old.dart +++ /dev/null @@ -1,1613 +0,0 @@ -import 'dart:math'; -import 'dart:ui'; -import 'package:Intaleq/constant/colors.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get.dart'; -import 'package:intaleq_maps/intaleq_maps.dart'; -import 'package:Intaleq/env/env.dart'; - -import '../../../constant/box_name.dart'; -import '../../../main.dart'; -import '../../widgets/error_snakbar.dart'; -import 'navigation_controller.dart'; - -// ─── Color Palette Matching the HTML Specs ────────────────────────────────── -Color get _kSurface => - Get.isDarkMode ? const Color(0xFF131b2e) : const Color(0xFFfaf8ff); -Color get _kSurfaceContainerLowest => - Get.isDarkMode ? const Color(0xFF1e293b) : const Color(0xFFffffff); -Color get _kSurfaceContainerHigh => - Get.isDarkMode ? const Color(0xFF334155) : const Color(0xFFe2e7ff); -Color get _kPrimary => const Color(0xFF000000); -Color get _kPrimaryContainer => const Color(0xFF131b2e); -Color get _kOnPrimaryContainer => const Color(0xFF7c839b); -Color get _kOnSurface => - Get.isDarkMode ? const Color(0xFFffffff) : const Color(0xFF131b2e); -Color get _kOnSurfaceVariant => const Color(0xFF45464d); -Color get _kErrorContainer => const Color(0xFFffdad6); -Color get _kError => const Color(0xFFba1a1a); -Color get _kOutlineVariant => const Color(0xFFc6c6cd); - -class NavigationView extends StatelessWidget { - const NavigationView({super.key}); - - @override - Widget build(BuildContext context) { - final NavigationController c = Get.put(NavigationController()); - - return AnnotatedRegion( - value: Get.isDarkMode - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark, - child: Scaffold( - backgroundColor: _kSurface, - body: GetBuilder( - builder: (_) => Stack( - children: [ - // ── 1. Map Layer ────────────────────────────────────────────── - IntaleqMap( - apiKey: Env.mapSaasKey, - onMapCreated: c.onMapCreated, - - onLongPress: (pos) => c.onMapLongPressed( - Point(0, 0), pos), // Adapted for IntaleqMap API - onTap: (pos) => c.onMapTapped(Point(0, 0), pos), - markers: c.markers, - polylines: c.polylines, - circles: c.circles, - polygons: c.polygons, - mapType: Get.isDarkMode - ? IntaleqMapType.normal - : IntaleqMapType.light, - initialCameraPosition: CameraPosition( - target: c.myLocation ?? const LatLng(33.5138, 36.2765), - zoom: 16.0), - myLocationEnabled: true, - ), - - // ── 2. Top UI (Explore Mode) ────────────────────────────────── - if (!c.isNavigating) _ExploreTopUI(controller: c), - - // ── 3. Top UI (Active Navigation Banner) ────────────────────── - if (c.isNavigating && c.currentInstruction.isNotEmpty) - _ActiveTopInstruction(controller: c), - - // ── 4. Explore Action Row (Capsules) ────────────────────────── - if (!c.isNavigating) _ExploreActionRow(controller: c), - - // ── 5. Bottom Panel (Explore Mode / Route Setup) ────────────── - if (!c.isNavigating) _ExploreBottomPanel(controller: c), - - // ── 6. Bottom HUD (Active Navigation) ───────────────────────── - if (c.isNavigating) _ActiveBottomHUD(controller: c), - - // ── 7. Speedometer Badge (Bottom) ────────────────────────────── - if (c.isNavigating) _SpeedBadge(speed: c.currentSpeed), - - // ── 8. Search Results Dropdown ──────────────────────────────── - if (c.placesDestination.isNotEmpty && !c.isNavigating) - _SearchResults(controller: c), - - // ── 9. Loading Overlay ──────────────────────────────────────── - if (c.isLoading) const _LoadingOverlay(), - - // ── 10. Location Picker Overlay (Place Creation) ────────────── - _LocationPickerOverlay(controller: c), - - // ── 11. Recenter / Follow Me Button ─────────────────────────── - if (!c.isCameraLocked) - Positioned( - right: 16, - bottom: c.isNavigating ? 140 : 250, - child: FloatingActionButton.small( - onPressed: () => c.relockCameraToUser(), - backgroundColor: Colors.white, - child: const Icon(Icons.my_location_rounded, - color: Color(0xFF0D47A1)), - ), - ), - ], - ), - ), - ), - ); - } -} - -// ============================================================================= -// EXPLORE MODE COMPONENTS -// ============================================================================= - -class _ExploreTopUI extends StatelessWidget { - final NavigationController controller; - const _ExploreTopUI({required this.controller}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: 0, - left: 0, - right: 0, - child: SafeArea( - bottom: false, - child: Column( - children: [ - // Search Pill - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), - child: ClipRRect( - borderRadius: BorderRadius.circular(28), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: _kSurfaceContainerLowest.withOpacity(0.85), - borderRadius: BorderRadius.circular(28), - border: Border.all( - color: Colors.white.withOpacity(0.3), width: 1.5), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.12), - blurRadius: 32, - offset: const Offset(0, 12), - ) - ], - ), - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - child: Row( - children: [ - IconButton( - icon: Icon(Icons.menu_rounded, - color: _kOnSurface, size: 26), - onPressed: () {}, // Drawer or Menu logic here - ), - const SizedBox(width: 8), - Expanded( - child: TextField( - controller: controller.placeDestinationController, - onChanged: controller.onSearchChanged, - textInputAction: TextInputAction.search, - style: TextStyle( - fontSize: 18, - color: _kOnSurface, - fontWeight: FontWeight.w700), - decoration: InputDecoration( - hintText: box.read(BoxName.lang) == 'ar' - ? 'إلى أين؟' - : 'Where to?', - hintStyle: TextStyle( - color: _kOnSurfaceVariant.withOpacity(0.7), - fontSize: 18, - fontWeight: FontWeight.w600), - border: InputBorder.none, - isDense: true, - contentPadding: - const EdgeInsets.symmetric(vertical: 12), - ), - ), - ), - if (controller - .placeDestinationController.text.isNotEmpty || - controller.routes.isNotEmpty) - Container( - decoration: const BoxDecoration( - color: Color(0xFFFFEBEE), - shape: BoxShape.circle, - ), - child: IconButton( - icon: const Icon(Icons.close_rounded), - color: Colors.red, - onPressed: () => controller.clearEverything(), - ), - ) - else - // Avatar - const Padding( - padding: EdgeInsets.only(right: 4), - child: CircleAvatar( - radius: 20, - backgroundColor: Color(0xFF0D47A1), - child: Icon(Icons.person_rounded, - color: Colors.white, size: 22), - ), - ), - ], - ), - ), - ), - ), - ), - ], - ), - ), - ); - } -} - -class _ExploreBottomPanel extends StatelessWidget { - final NavigationController controller; - const _ExploreBottomPanel({required this.controller}); - - String _formatDuration(double seconds) { - final mins = (seconds / 60).toInt(); - if (mins >= 60) { - final h = mins ~/ 60; - final m = mins % 60; - return box.read(BoxName.lang) == 'ar' - ? '$h ساعة ${m > 0 ? '$m د' : ''}' - : '${h}h ${m > 0 ? '${m}m' : ''}'; - } - return box.read(BoxName.lang) == 'ar' ? '$mins دقيقة' : '$mins min'; - } - - String _formatDistance(double meters) { - if (meters >= 1000) { - return box.read(BoxName.lang) == 'ar' - ? '${(meters / 1000).toStringAsFixed(1)} كم' - : '${(meters / 1000).toStringAsFixed(1)} km'; - } - return box.read(BoxName.lang) == 'ar' - ? '${meters.toInt()} م' - : '${meters.toInt()} m'; - } - - @override - Widget build(BuildContext context) { - final bool hasRoutes = controller.routes.isNotEmpty; - final bool isArabic = box.read(BoxName.lang) == 'ar'; - final bottomPad = MediaQuery.of(context).padding.bottom; - - if (!hasRoutes && controller.recentLocations.isEmpty) { - return const SizedBox.shrink(); - } - - return Positioned( - bottom: 0, - left: 0, - right: 0, - child: ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), - child: Container( - padding: EdgeInsets.only(bottom: bottomPad + 8), - decoration: BoxDecoration( - color: _kSurfaceContainerLowest.withOpacity(0.92), - boxShadow: const [ - BoxShadow( - color: Color(0x14000000), - blurRadius: 48, - offset: Offset(0, -12)) - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Minimal Drag Handle - Container( - width: 32, - height: 3, - margin: const EdgeInsets.only(top: 8, bottom: 8), - decoration: BoxDecoration( - color: _kOutlineVariant.withOpacity(0.3), - borderRadius: BorderRadius.circular(10))), - - // ── Route Selection Cards ── - if (hasRoutes) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - children: - List.generate(controller.routes.length, (index) { - final r = controller.routes[index]; - final isSelected = - controller.selectedRouteIndex == index; - return _RouteOptionCard( - index: index, - distance: _formatDistance(r.distanceM), - duration: _formatDuration(r.durationS), - isSelected: isSelected, - isArabic: isArabic, - onTap: () => controller.selectRoute(index), - ); - }), - ), - ), - const SizedBox(height: 12), - // Start Button - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - HapticFeedback.mediumImpact(); - controller.isNavigating = true; - controller.relockCameraToUser(); - controller.update(); - }, - borderRadius: BorderRadius.circular(16), - child: Ink( - padding: const EdgeInsets.symmetric(vertical: 16), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFF0D47A1), Color(0xFF1565C0)], - ), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: - const Color(0xFF0D47A1).withOpacity(0.35), - blurRadius: 16, - offset: const Offset(0, 6)) - ], - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.navigation_rounded, - color: Colors.white, size: 20), - const SizedBox(width: 8), - Text( - isArabic - ? 'ابدأ الملاحة' - : 'Start Navigation', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w800)), - ], - ), - ), - ), - ), - ), - ] else if (!hasRoutes) ...[ - const SizedBox.shrink(), - ], - ], - ), - ), - ), - ), - ); - } -} - -class _RouteOptionCard extends StatelessWidget { - final int index; - final String distance; - final String duration; - final bool isSelected; - final bool isArabic; - final VoidCallback onTap; - - const _RouteOptionCard({ - required this.index, - required this.distance, - required this.duration, - required this.isSelected, - required this.isArabic, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - HapticFeedback.selectionClick(); - onTap(); - }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - decoration: BoxDecoration( - color: isSelected - ? const Color(0xFF0D47A1) - : _kSurfaceContainerHigh.withOpacity(0.4), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isSelected - ? const Color(0xFF42A5F5).withOpacity(0.4) - : _kOutlineVariant.withOpacity(0.15), - width: isSelected ? 1.5 : 1, - ), - ), - child: Row( - children: [ - // Route icon - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: isSelected - ? Colors.white.withOpacity(0.15) - : _kSurfaceContainerHigh.withOpacity(0.5), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - index == 0 ? Icons.route_rounded : Icons.alt_route_rounded, - color: isSelected ? Colors.white : _kOnSurfaceVariant, - size: 20, - ), - ), - const SizedBox(width: 14), - - // Route Label - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - index == 0 - ? (isArabic ? 'أفضل مسار' : 'Best Route') - : (isArabic - ? 'مسار بديل $index' - : 'Alternative $index'), - style: TextStyle( - color: isSelected ? Colors.white : _kOnSurface, - fontSize: 14, - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 2), - Text( - distance, - style: TextStyle( - color: isSelected - ? Colors.white.withOpacity(0.7) - : _kOnSurfaceVariant, - fontSize: 12, - ), - ), - ], - ), - ), - - // Duration (prominent) - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: isSelected - ? Colors.white.withOpacity(0.15) - : const Color(0xFFE3F2FD), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - duration, - style: TextStyle( - color: isSelected ? Colors.white : const Color(0xFF0D47A1), - fontSize: 14, - fontWeight: FontWeight.w800, - ), - ), - ), - - // Selection indicator - const SizedBox(width: 8), - Icon( - isSelected - ? Icons.check_circle_rounded - : Icons.radio_button_unchecked_rounded, - color: isSelected ? Colors.white : _kOutlineVariant, - size: 22, - ), - ], - ), - ), - ); - } -} - -class _CompactRecentPlace extends StatelessWidget { - final String title; - final String subtitle; - final VoidCallback onTap; - - const _CompactRecentPlace({ - required this.title, - required this.subtitle, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return ListTile( - onTap: onTap, - dense: true, - contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 0), - leading: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: _kSurfaceContainerHigh.withOpacity(0.5), - shape: BoxShape.circle, - ), - child: Icon(Icons.history_rounded, size: 18, color: _kOnSurface), - ), - title: Text(title, - style: TextStyle( - fontWeight: FontWeight.w700, color: _kOnSurface, fontSize: 14)), - subtitle: Text(subtitle, - style: TextStyle(color: _kOnSurfaceVariant, fontSize: 12), - maxLines: 1, - overflow: TextOverflow.ellipsis), - ); - } -} - -// ============================================================================= -// ACTIVE NAVIGATION MODE COMPONENTS -// ============================================================================= - -class _ActiveTopInstruction extends StatelessWidget { - final NavigationController controller; - const _ActiveTopInstruction({required this.controller}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: MediaQuery.of(context).padding.top + 12, - left: 16, - right: 16, - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFF1B5E20), Color(0xFF2E7D32)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(24), - border: - Border.all(color: Colors.white.withOpacity(0.2), width: 1), - boxShadow: const [ - BoxShadow( - color: Color(0x33000000), - blurRadius: 24, - offset: Offset(0, 12)) - ], - ), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: Row( - children: [ - // Direction indicator - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - shape: BoxShape.circle, - ), - child: Icon(controller.currentManeuverIcon, - color: Colors.white, size: 36), - ), - const SizedBox(width: 16), - // Full instruction text - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - controller.currentInstruction, - style: const TextStyle( - color: Colors.white, - fontSize: 20, - fontWeight: FontWeight.w800, - letterSpacing: -0.5), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 2), - Text( - controller.distanceToNextStep, - style: TextStyle( - color: Colors.white.withOpacity(0.9), - fontSize: 16, - fontWeight: FontWeight.w700), - ), - ], - ), - ), - ], - ), - ), - ), - ), - ); - } -} - -class _ActiveBottomHUD extends StatelessWidget { - final NavigationController controller; - const _ActiveBottomHUD({required this.controller}); - - @override - Widget build(BuildContext context) { - final bottomPad = MediaQuery.of(context).padding.bottom; - final isArabic = box.read(BoxName.lang) == 'ar'; - - return Positioned( - bottom: bottomPad + 16, - left: 16, - right: 16, - child: ClipRRect( - borderRadius: BorderRadius.circular(32), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 16, sigmaY: 16), - child: Container( - decoration: BoxDecoration( - color: _kSurfaceContainerLowest.withOpacity(0.85), - borderRadius: BorderRadius.circular(32), - border: - Border.all(color: Colors.white.withOpacity(0.4), width: 1.5), - boxShadow: const [ - BoxShadow( - color: Color(0x14000000), - blurRadius: 24, - offset: Offset(0, 12)) - ], - ), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Stats Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Text( - controller.arrivalTime, - style: const TextStyle( - fontSize: 22, - fontWeight: FontWeight.w900, - color: Color(0xFF0D47A1), - ), - ), - Container( - width: 1.5, - height: 24, - color: _kOutlineVariant.withOpacity(0.3)), - Text( - isArabic - ? '${controller.estimatedTimeRemaining} دقيقة' - : '${controller.estimatedTimeRemaining} min', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - color: _kOnSurface, - ), - ), - Container( - width: 1.5, - height: 24, - color: _kOutlineVariant.withOpacity(0.3)), - Text( - controller.distanceWithUnit, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w700, - color: _kOnSurfaceVariant, - ), - ), - ], - ), - const SizedBox(height: 16), - // Action Buttons Row - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _IconBtn( - icon: Icons.report_problem_rounded, - bgColor: const Color(0xFFFFF3E0), - iconColor: Colors.orange.shade800, - onTap: () {}, - ), - _IconBtn( - icon: controller.isMuted - ? Icons.volume_off_rounded - : Icons.volume_up_rounded, - bgColor: const Color(0xFFE3F2FD), - iconColor: const Color(0xFF1976D2), - onTap: () => controller.toggleMute(), - ), - _IconBtn( - icon: Icons.add_rounded, - bgColor: const Color(0xFFE8F5E9), - iconColor: const Color(0xFF388E3C), - onTap: () => controller.togglePlaceSelectionMode(), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: GestureDetector( - onTap: () => controller.clearRoute(), - child: Container( - height: 48, - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [Color(0xFFE53935), Color(0xFFC62828)], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(24), - boxShadow: [ - BoxShadow( - color: Colors.red.withOpacity(0.3), - blurRadius: 12, - offset: const Offset(0, 6), - ) - ], - ), - child: Center( - child: Text( - isArabic ? 'إنهاء الملاحة' : 'End Route', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - letterSpacing: 0.5, - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ); - } -} - -class _IconBtn extends StatelessWidget { - final IconData icon; - final Color bgColor; - final Color iconColor; - final VoidCallback onTap; - - const _IconBtn( - {required this.icon, - required this.bgColor, - required this.iconColor, - required this.onTap}); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - HapticFeedback.lightImpact(); - onTap(); - }, - child: Container( - width: 48, - height: 48, - margin: const EdgeInsets.only(right: 12), - decoration: BoxDecoration( - color: bgColor, - shape: BoxShape.circle, - ), - child: Icon(icon, color: iconColor, size: 24), - ), - ); - } -} - -void _showAddPlaceFormDialog( - BuildContext context, NavigationController controller) { - final nameController = TextEditingController(); - final categoryNotifier = ValueNotifier?>(null); - final isAr = box.read(BoxName.lang) == 'ar'; - - Get.dialog( - AlertDialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), - backgroundColor: _kSurfaceContainerLowest, - title: Row( - children: [ - const Icon(Icons.add_business_rounded, - color: Color(0xFF0D47A1), size: 28), - const SizedBox(width: 12), - Text(isAr ? 'إضافة مكان جديد' : 'Add New Place', - style: - TextStyle(color: _kOnSurface, fontWeight: FontWeight.bold)), - ], - ), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - isAr - ? "ساهم في تحسين الخريطة بإضافة الأماكن الناقصة." - : "Help improve the map by adding missing places.", - style: TextStyle(color: _kOnSurfaceVariant, fontSize: 13), - ), - const SizedBox(height: 20), - TextField( - controller: nameController, - style: TextStyle(color: _kOnSurface), - decoration: InputDecoration( - labelText: isAr ? 'اسم المكان' : 'Place Name', - labelStyle: TextStyle(color: _kOnSurfaceVariant), - prefixIcon: - Icon(Icons.label_rounded, color: _kOnSurfaceVariant), - filled: true, - fillColor: _kSurfaceContainerHigh.withOpacity(0.3), - border: - OutlineInputBorder(borderRadius: BorderRadius.circular(12)), - ), - ), - const SizedBox(height: 16), - - // Category Picker Trigger - ValueListenableBuilder?>( - valueListenable: categoryNotifier, - builder: (context, selected, _) { - return InkWell( - onTap: () => _showCategoryPicker(context, (cat) { - categoryNotifier.value = cat; - }), - borderRadius: BorderRadius.circular(12), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 16), - decoration: BoxDecoration( - color: _kSurfaceContainerHigh.withOpacity(0.3), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: _kOutlineVariant.withOpacity(0.5)), - ), - child: Row( - children: [ - Icon(Icons.category_rounded, - color: _kOnSurfaceVariant), - const SizedBox(width: 12), - Expanded( - child: Text( - selected != null - ? (isAr ? selected['ar']! : selected['en']!) - : (isAr ? 'اختر الفئة' : 'Select Category'), - style: TextStyle( - color: selected != null - ? _kOnSurface - : _kOnSurfaceVariant, - fontSize: 16, - ), - ), - ), - Icon(Icons.keyboard_arrow_down_rounded, - color: _kOnSurfaceVariant), - ], - ), - ), - ); - }), - - const SizedBox(height: 24), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF0D47A1), - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12)), - ), - onPressed: () { - if (nameController.text.isNotEmpty && - categoryNotifier.value != null) { - Get.back(); - controller.submitNewPlace( - nameController.text, categoryNotifier.value!['id']!); - } else { - mySnackbarWarning( - isAr ? 'يرجى إكمال البيانات' : 'Please fill all fields'); - } - }, - child: Text(isAr ? 'إرسال' : 'Submit', - style: const TextStyle( - color: Colors.white, fontWeight: FontWeight.bold)), - ), - TextButton( - onPressed: () => Get.back(), - child: Text(isAr ? 'إلغاء' : 'Cancel', - style: TextStyle(color: _kOnSurfaceVariant)), - ), - ], - ), - ), - ), - ); -} - -void _showCategoryPicker( - BuildContext context, Function(Map) onSelected) { - final isAr = box.read(BoxName.lang) == 'ar'; - - Get.bottomSheet( - Container( - decoration: BoxDecoration( - color: _kSurfaceContainerLowest, - borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), - ), - padding: const EdgeInsets.only(top: 12, bottom: 24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: _kOutlineVariant.withOpacity(0.3), - borderRadius: BorderRadius.circular(2), - ), - ), - const SizedBox(height: 16), - Text(isAr ? 'اختر الفئة' : 'Select Category', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: _kOnSurface)), - const SizedBox(height: 16), - Flexible( - child: ListView.builder( - shrinkWrap: true, - itemCount: NavigationController.placeCategories.length, - itemBuilder: (context, index) { - final cat = NavigationController.placeCategories[index]; - return ListTile( - leading: Icon(_getIconData(cat['icon']!), - color: const Color(0xFF0D47A1)), - title: Text(isAr ? cat['ar']! : cat['en']!, - style: TextStyle( - color: _kOnSurface, fontWeight: FontWeight.w600)), - onTap: () { - HapticFeedback.lightImpact(); - onSelected(cat); - Get.back(); - }, - ); - }, - ), - ), - ], - ), - ), - isScrollControlled: true, - ); -} - -IconData _getIconData(String name) { - switch (name) { - case 'restaurant': - return Icons.restaurant_rounded; - case 'coffee': - return Icons.coffee_rounded; - case 'shopping_basket': - return Icons.shopping_basket_rounded; - case 'local_pharmacy': - return Icons.local_pharmacy_rounded; - case 'local_gas_station': - return Icons.local_gas_station_rounded; - case 'atm': - return Icons.atm_rounded; - case 'account_balance': - return Icons.account_balance_rounded; - case 'mosque': - return Icons.mosque_rounded; - case 'local_hospital': - return Icons.local_hospital_rounded; - case 'school': - return Icons.school_rounded; - case 'park': - return Icons.park_rounded; - case 'hotel': - return Icons.hotel_rounded; - case 'shopping_mall': - return Icons.store_rounded; - case 'fitness_center': - return Icons.fitness_center_rounded; - case 'content_cut': - return Icons.content_cut_rounded; - case 'bakery_dining': - return Icons.bakery_dining_rounded; - case 'local_laundry_service': - return Icons.local_laundry_service_rounded; - case 'build': - return Icons.build_rounded; - case 'gavel': - return Icons.gavel_rounded; - default: - return Icons.place_rounded; - } -} - -class _LocationPickerOverlay extends StatelessWidget { - final NavigationController controller; - const _LocationPickerOverlay({required this.controller}); - - @override - Widget build(BuildContext context) { - if (!controller.isSelectingPlaceLocation) return const SizedBox.shrink(); - - final isAr = box.read(BoxName.lang) == 'ar'; - - return Stack( - children: [ - // Dim the background slightly - Non-blocking - IgnorePointer( - child: Container(color: Colors.black.withOpacity(0.1)), - ), - - // Center Crosshair/Pointer - Non-blocking - IgnorePointer( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.9), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 10, - offset: const Offset(0, 4)) - ], - ), - child: const Icon(Icons.add_location_alt_rounded, - color: Color(0xFF0D47A1), size: 40), - ), - const SizedBox(height: 40), - ], - ), - ), - ), - - // Confirm Button - Positioned( - bottom: 110, - left: 32, - right: 32, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF1B5E20), - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16)), - elevation: 8, - shadowColor: const Color(0xFF1B5E20).withOpacity(0.5), - ), - onPressed: () => _showAddPlaceFormDialog(context, controller), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.check_circle_rounded, color: Colors.white), - const SizedBox(width: 12), - Text( - isAr ? 'تأكيد الموقع' : 'Confirm Location', - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.w800), - ), - ], - ), - ), - ), - - // Help Tooltip - Positioned( - top: 140, - left: 40, - right: 40, - child: IgnorePointer( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), - borderRadius: BorderRadius.circular(30), - ), - child: Text( - isAr - ? "حرك الخريطة لتحديد موقع المكان" - : "Move map to pick place location", - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white, fontSize: 13), - ), - ), - ), - ), - - // Cancel Button - Positioned( - top: 60, - right: 20, - child: FloatingActionButton.small( - backgroundColor: Colors.white, - elevation: 4, - child: const Icon(Icons.close_rounded, color: Colors.black87), - onPressed: () => controller.togglePlaceSelectionMode(), - ), - ), - ], - ); - } -} - -class _StatItem extends StatelessWidget { - final String label; - final String value; - final Color color; - const _StatItem( - {required this.label, required this.value, required this.color}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Text(label, - style: TextStyle( - fontSize: 10, - color: _kOnSurfaceVariant, - fontWeight: FontWeight.bold)), - Text(value, - style: TextStyle( - fontSize: 16, color: color, fontWeight: FontWeight.w900)), - ], - ); - } -} - -class _ActionButton extends StatelessWidget { - final IconData icon; - final String label; - final Color color; - final Color? iconColor; - final VoidCallback onPressed; - const _ActionButton( - {required this.icon, - required this.label, - required this.color, - this.iconColor, - required this.onPressed}); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - IconButton( - onPressed: onPressed, - icon: Icon(icon, color: iconColor ?? _kOnSurface), - style: IconButton.styleFrom( - backgroundColor: color, fixedSize: const Size(50, 50)), - ), - const SizedBox(height: 4), - Text(label, style: const TextStyle(fontSize: 11)), - ], - ); - } -} - -class _SpeedBadge extends StatelessWidget { - final double speed; - const _SpeedBadge({required this.speed}); - - @override - Widget build(BuildContext context) { - final int kmh = speed.toInt(); - // Dynamic border color based on speed - final Color borderColor = kmh > 80 ? const Color(0xFFFF5252) : const Color(0xFFE53935); - - // Positioned at the bottom (above the HUD) - return Positioned( - bottom: 120, // Above the _ActiveBottomHUD - left: 24, - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: _kSurfaceContainerLowest, - shape: BoxShape.circle, - border: Border.all(color: borderColor, width: 4), // Red border - boxShadow: const [ - BoxShadow( - color: Color(0x0F000000), blurRadius: 32, offset: Offset(0, 8)) - ], - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('$kmh', - style: TextStyle( - color: borderColor, // Speed number in red - fontSize: 28, - fontWeight: FontWeight.w900, - height: 1.0)), - Text('km/h', - style: TextStyle( - color: _kOnSurfaceVariant, - fontSize: 10, - fontWeight: FontWeight.w800, - letterSpacing: -0.5, - )), - ], - ), - ), - ); - } -} - -// ============================================================================= -// SHARED UTILITIES -// ============================================================================= - -class _MapFab extends StatelessWidget { - final IconData icon; - final Color iconColor; - final VoidCallback onTap; - const _MapFab( - {required this.icon, required this.iconColor, required this.onTap}); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), - child: Container( - width: 50, // Slightly smaller - height: 50, - decoration: BoxDecoration( - color: _kSurfaceContainerLowest.withOpacity(0.95), - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow( - color: Color(0x1A000000), - blurRadius: 16, - offset: Offset(0, 8)) - ]), - child: Icon(icon, color: iconColor, size: 24), - ), - ), - ), - ); - } -} - -class _ExploreActionRow extends StatelessWidget { - final NavigationController controller; - const _ExploreActionRow({required this.controller}); - - @override - Widget build(BuildContext context) { - final bool hasRoutes = controller.routes.isNotEmpty; - final bool hasRecents = controller.recentLocations.isNotEmpty; - final isAr = box.read(BoxName.lang) == 'ar'; - - final double safeBottom = MediaQuery.of(context).padding.bottom; - final double bottomOffset = safeBottom + 20; - - return AnimatedPositioned( - duration: const Duration(milliseconds: 300), - curve: Curves.easeOutCubic, - bottom: bottomOffset, - left: 0, - right: 0, - child: Container( - color: Colors.transparent, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // Row 1: Favorites - SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16), - physics: const BouncingScrollPhysics(), - child: Row( - children: [ - _ActionCapsule( - icon: Icons.add_rounded, - label: isAr ? 'إضافة' : 'Add', - onTap: () => controller.togglePlaceSelectionMode(), - isPrimary: true, - ), - _ActionCapsule( - icon: Icons.home_rounded, - label: isAr ? 'المنزل' : 'Home', - onTap: () => controller.goToFavorite('home'), - ), - _ActionCapsule( - icon: Icons.work_rounded, - label: isAr ? 'العمل' : 'Work', - onTap: () => controller.goToFavorite('work'), - ), - _ActionCapsule( - icon: Icons.bookmark_rounded, - label: isAr ? 'المحفوظات' : 'Saved', - onTap: () {}, - ), - _ActionCapsule( - icon: Icons.flight_rounded, - label: isAr ? 'المطار' : 'Airport', - onTap: () => controller.goToFavorite('airport'), - ), - const SizedBox(width: 8), - _MapFab( - icon: Icons.my_location_rounded, - iconColor: controller.isCameraLocked - ? const Color(0xFF0D47A1) - : Colors.grey[400]!, - onTap: () { - HapticFeedback.lightImpact(); - controller.relockCameraToUser(); - }, - ), - ], - ), - ), - // Row 2: Recent History (if any) - if (hasRecents && !hasRoutes) ...[ - const SizedBox(height: 12), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - physics: const BouncingScrollPhysics(), - child: Row( - children: controller.recentLocations.take(5).map((place) { - return _ActionCapsule( - icon: Icons.history_rounded, - label: place['name'] ?? '', - onTap: () => controller.selectDestination(place), - isRecent: true, - ); - }).toList(), - ), - ), - ], - - const SizedBox(height: 12), - ], - ), - ), - ); - } -} - -class _ActionCapsule extends StatelessWidget { - final IconData icon; - final String label; - final VoidCallback onTap; - final bool isPrimary; - final bool isRecent; - - const _ActionCapsule({ - required this.icon, - required this.label, - required this.onTap, - this.isPrimary = false, - this.isRecent = false, - }); - - @override - Widget build(BuildContext context) { - Color bgColor; - Color textColor; - - if (isPrimary) { - bgColor = const Color(0xFF0D47A1).withOpacity(0.9); - textColor = Colors.white; - } else if (isRecent) { - bgColor = _kSurfaceContainerHigh.withOpacity(0.8); - textColor = _kOnSurfaceVariant; - } else { - bgColor = _kSurfaceContainerLowest.withOpacity(0.85); - textColor = _kOnSurface; - } - - return Padding( - padding: const EdgeInsets.only(right: 8), - child: GestureDetector( - onTap: () { - HapticFeedback.lightImpact(); - onTap(); - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(50), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: bgColor, - borderRadius: BorderRadius.circular(50), - border: Border.all( - color: isRecent - ? Colors.transparent - : Colors.white.withOpacity(0.2), - width: 1.2), - boxShadow: const [ - BoxShadow( - color: Color(0x14000000), - blurRadius: 8, - offset: Offset(0, 4)) - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 18, color: textColor), - const SizedBox(width: 8), - Text( - label, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: textColor, - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} - -class _SearchResults extends StatelessWidget { - final NavigationController controller; - const _SearchResults({required this.controller}); - - @override - Widget build(BuildContext context) { - return Positioned( - top: MediaQuery.of(context).padding.top + 90, - left: 16, - right: 16, - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), - child: Container( - decoration: BoxDecoration( - color: _kSurfaceContainerLowest.withOpacity(0.95), - borderRadius: BorderRadius.circular(24), - border: Border.all(color: Colors.white.withOpacity(0.2)), - boxShadow: const [ - BoxShadow( - color: Color(0x1A000000), - blurRadius: 32, - offset: Offset(0, 16)) - ]), - padding: const EdgeInsets.symmetric(vertical: 8), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 300), - child: ListView.separated( - shrinkWrap: true, - padding: EdgeInsets.zero, - itemCount: controller.placesDestination.length, - separatorBuilder: (_, __) => Divider( - height: 1, - color: _kOutlineVariant.withOpacity(0.2), - indent: 72), - itemBuilder: (_, i) { - final place = controller.placesDestination[i]; - final dist = place['distanceKm'] as double?; - return InkWell( - onTap: () => controller.selectDestination(place), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, vertical: 16), - child: Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: _kSurfaceContainerHigh, - borderRadius: BorderRadius.circular(12)), - child: Icon(Icons.place_rounded, - color: _kOnSurfaceVariant, size: 20)), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(place['name'] ?? '', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: _kOnSurface), - maxLines: 1, - overflow: TextOverflow.ellipsis), - if ((place['address'] ?? '').isNotEmpty) - Text(place['address'], - style: TextStyle( - fontSize: 14, - color: _kOnSurfaceVariant), - maxLines: 1, - overflow: TextOverflow.ellipsis), - ], - ), - ), - if (dist != null) ...[ - const SizedBox(width: 12), - Text('${dist.toStringAsFixed(1)} km', - style: TextStyle( - color: _kOnSurfaceVariant, - fontSize: 14, - fontWeight: FontWeight.bold)), - ], - ], - ), - ), - ); - }, - ), - ), - ), - ), - ), - ); - } -} - -class _LoadingOverlay extends StatelessWidget { - const _LoadingOverlay(); - - @override - Widget build(BuildContext context) { - return Positioned.fill( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6), - child: Container( - color: _kPrimaryContainer.withOpacity(0.4), - child: Center( - child: Container( - padding: const EdgeInsets.all(32), - decoration: BoxDecoration( - color: _kSurfaceContainerLowest, - borderRadius: BorderRadius.circular(24), - boxShadow: const [ - BoxShadow( - color: Color(0x33000000), - blurRadius: 48, - offset: Offset(0, 16)) - ]), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.black), - strokeWidth: 4), - const SizedBox(height: 24), - Text('Routing...'.tr, - style: TextStyle( - color: _kOnSurface, - fontSize: 16, - fontWeight: FontWeight.w800)), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/views/widgets/voice_call_bottom_sheet.dart b/lib/views/widgets/voice_call_bottom_sheet.dart new file mode 100644 index 0000000..4752ea7 --- /dev/null +++ b/lib/views/widgets/voice_call_bottom_sheet.dart @@ -0,0 +1,290 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../constant/colors.dart'; +import '../../constant/style.dart'; +import '../../controller/voice_call_controller.dart'; + +class VoiceCallBottomSheet extends StatelessWidget { + const VoiceCallBottomSheet({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.find(); + final double screenHeight = MediaQuery.of(context).size.height; + final bool isDark = Theme.of(context).brightness == Brightness.dark; + + // Harmonious curated colors + final Color bgColor = isDark ? const Color(0xFF121212) : Colors.white; + final Color cardColor = isDark ? const Color(0xFF1E1E1E) : const Color(0xFFF5F5F7); + final Color textColor = isDark ? Colors.white : const Color(0xFF1C1C1E); + final Color subTextColor = isDark ? Colors.white70 : Colors.black54; + + return WillPopScope( + onWillPop: () async => false, + child: Container( + height: screenHeight * 0.9, + decoration: BoxDecoration( + color: bgColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(32)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 20, + offset: const Offset(0, -5), + ) + ], + ), + child: Obx(() { + final state = controller.state.value; + final seconds = controller.elapsedSeconds.value; + final remoteName = controller.remoteName.value; + final isMuted = controller.isMuted.value; + final isSpeakerOn = controller.isSpeakerOn.value; + + // Progress ring logic + final double progress = seconds / 60.0; + final Color ringColor = seconds > 10 ? const Color(0xFF2ECC71) : const Color(0xFFE74C3C); + + // Status text translations + String statusText = ""; + switch (state) { + case VoiceCallState.dialing: + statusText = "${'Calling'.tr} $remoteName..."; + break; + case VoiceCallState.ringing: + statusText = "${'Captain'.tr} $remoteName ${'is calling you'.tr}..."; + break; + case VoiceCallState.connecting: + statusText = "Connecting...".tr; + break; + case VoiceCallState.active: + statusText = "Call Connected".tr; + break; + case VoiceCallState.ended: + statusText = "Call Ended".tr; + break; + case VoiceCallState.idle: + statusText = ""; + break; + } + + return Column( + children: [ + // Top Drag Handle Indicator + Center( + child: Container( + margin: const EdgeInsets.only(top: 12, bottom: 24), + width: 44, + height: 5, + decoration: BoxDecoration( + color: isDark ? Colors.white24 : Colors.black12, + borderRadius: BorderRadius.circular(10), + ), + ), + ), + + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Header Info + Column( + children: [ + Text( + "Free Call".tr, + style: TextStyle( + color: ringColor, + fontWeight: FontWeight.w800, + fontSize: 14, + letterSpacing: 1.2, + ), + ), + const SizedBox(height: 8), + Text( + remoteName, + style: TextStyle( + color: textColor, + fontWeight: FontWeight.w900, + fontSize: 26, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + statusText, + style: TextStyle( + color: subTextColor, + fontWeight: FontWeight.w600, + fontSize: 16, + ), + ), + ], + ), + + // Avatar & Animated Progress Ring + Stack( + alignment: Alignment.center, + children: [ + // Progress ring around avatar (Active state only) + if (state == VoiceCallState.active) + SizedBox( + width: 172, + height: 172, + child: CircularProgressIndicator( + value: progress, + strokeWidth: 5, + backgroundColor: isDark ? Colors.white10 : Colors.black12, + valueColor: AlwaysStoppedAnimation(ringColor), + ), + ), + + // Main Avatar Card + Container( + width: 150, + height: 150, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: cardColor, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.08), + blurRadius: 15, + offset: const Offset(0, 8), + ) + ], + ), + child: Center( + child: remoteName.isNotEmpty + ? Text( + remoteName[0].toUpperCase(), + style: TextStyle( + color: textColor, + fontWeight: FontWeight.bold, + fontSize: 54, + ), + ) + : Icon( + Icons.person, + color: textColor.withOpacity(0.6), + size: 64, + ), + ), + ), + ], + ), + + // Timer Counter Display + if (state == VoiceCallState.active) + Text( + "0:${seconds.toString().padLeft(2, '0')}", + style: TextStyle( + color: seconds > 10 ? textColor : const Color(0xFFE74C3C), + fontWeight: FontWeight.bold, + fontSize: 22, + fontFamily: 'monospace', + ), + ) + else + const SizedBox(height: 24), + + // Action Controls Block + if (state == VoiceCallState.ringing) + // Incoming Ringing Controls: Accept / Decline + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildCircleActionButton( + icon: Icons.call_end_rounded, + color: Colors.white, + bgColor: const Color(0xFFE74C3C), + onTap: () => controller.declineCall(), + label: "Decline".tr, + ), + _buildCircleActionButton( + icon: Icons.call_rounded, + color: Colors.white, + bgColor: const Color(0xFF2ECC71), + onTap: () => controller.acceptCall(), + label: "Accept".tr, + ), + ], + ) + else + // Dialing or Connected Controls: Speaker / Mute / Hangup + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Speakerphone toggle + _buildCircleActionButton( + icon: isSpeakerOn ? Icons.volume_up_rounded : Icons.volume_down_rounded, + color: isSpeakerOn ? Colors.white : textColor, + bgColor: isSpeakerOn ? const Color(0xFF2ECC71) : cardColor, + onTap: () => controller.toggleSpeaker(), + label: "Speaker".tr, + ), + // Hangup Call + _buildCircleActionButton( + icon: Icons.call_end_rounded, + color: Colors.white, + bgColor: const Color(0xFFE74C3C), + onTap: () => controller.hangup(), + label: "End".tr, + ), + // Mute Microphone + _buildCircleActionButton( + icon: isMuted ? Icons.mic_off_rounded : Icons.mic_rounded, + color: isMuted ? Colors.white : textColor, + bgColor: isMuted ? const Color(0xFFE74C3C) : cardColor, + onTap: () => controller.toggleMute(), + label: "Mute".tr, + ), + ], + ), + ], + ), + ), + ), + ], + ); + }), + ), + ); +} + + Widget _buildCircleActionButton({ + required IconData icon, + required Color color, + required Color bgColor, + required VoidCallback onTap, + required String label, + }) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + onPressed: onTap, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(18), + backgroundColor: bgColor, + foregroundColor: color, + elevation: 2, + ), + child: Icon(icon, size: 28), + ), + const SizedBox(height: 8), + Text( + label, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + ], + ); + } +} diff --git a/packages/flutter_paypal/lib/flutter_paypal.dart b/packages/flutter_paypal/lib/flutter_paypal.dart index 9725ac2..a318e76 100644 --- a/packages/flutter_paypal/lib/flutter_paypal.dart +++ b/packages/flutter_paypal/lib/flutter_paypal.dart @@ -7,8 +7,6 @@ import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:webview_flutter/webview_flutter.dart'; // Import for Android features. import 'package:webview_flutter_android/webview_flutter_android.dart'; -// Import for iOS features. -import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; import 'src/PaypalServices.dart'; import 'src/errors/network_error.dart'; @@ -128,20 +126,10 @@ class UsePaypalState extends State { // Enable hybrid composition. loadPayment(); - // #docregion platform_features - late final PlatformWebViewControllerCreationParams params; - if (WebViewPlatform.instance is WebKitWebViewPlatform) { - params = WebKitWebViewControllerCreationParams( - allowsInlineMediaPlayback: true, - mediaTypesRequiringUserAction: const {}, - ); - } else { - params = const PlatformWebViewControllerCreationParams(); - } - + const PlatformWebViewControllerCreationParams params = + PlatformWebViewControllerCreationParams(); final WebViewController controller = WebViewController.fromPlatformCreationParams(params); - // #enddocregion platform_features controller ..setJavaScriptMode(JavaScriptMode.unrestricted) diff --git a/plan/intaleq_admin_analysis_report.md b/plan/intaleq_admin_analysis_report.md new file mode 100644 index 0000000..205d07a --- /dev/null +++ b/plan/intaleq_admin_analysis_report.md @@ -0,0 +1,701 @@ +
+ +# تقرير تحليل شامل لتطبيق إنتلق - لوحة الإدارة (Intaleq Admin) + +--- + +## 📋 فهرس المحتويات + +1. [نظرة عامة على النظام](#1-نظرة-عامة-على-النظام) +2. [هيكل المشاريع والهندسة المعمارية](#2-هيكل-المشاريع-والهندسة-المعمارية) +3. [تحليل الباك إند - الخادم الرئيسي (intaleq_v1)](#3-تحليل-الباك-إند---الخادم-الرئيسي) +4. [تحليل لوحة الإدارة (intaleq_admin)](#4-تحليل-لوحة-الإدارة) +5. [تحليل تطبيق خدمة العملاء (service_intaleq)](#5-تحليل-تطبيق-خدمة-العملاء) +6. [قاعدة البيانات والموديلات](#6-قاعدة-البيانات-والموديلات) +7. [نظام المصادقة والصلاحيات](#7-نظام-المصادقة-والصلاحيات) +8. [تحليل التوافقية بين الباك إند والأدمن](#8-تحليل-التوافقية-بين-الباك-إند-والأدمن) +9. [الإضافات والتحسينات المقترحة](#9-الإضافات-والتحسينات-المقترحة) +10. [ملخص وتوصيات](#10-ملخص-وتوصيات) + +--- + +## 1. نظرة عامة على النظام + +### 1.1 وصف النظام + +نظام **إنتلق (Intaleq)** هو منصة متكاملة لخدمات نقل الركاب (ride-hailing) تعمل في عدة دول. يتكون النظام من المكونات الرئيسية التالية: + +| المكون | التقنية | الوصف | +|-------|---------|-------| +| **تطبيق الركاب (Intaleq)** | Flutter/Dart | تطبيق المستخدمين لطلب الرحلات | +| **تطبيق السائقين (intaleq_driver)** | Flutter/Dart | تطبيق السائقين (الكباتن) لاستقبال وتنفيذ الرحلات | +| **لوحة الإدارة (intaleq_admin)** | Flutter/Dart | لوحة تحكم للمشرفين لإدارة النظام | +| **تطبيق خدمة العملاء (service_intaleq)** | Flutter/Dart | تطبيق للموظفين للدعم وإدارة السائقين | +| **الخادم الرئيسي (intaleq_v1)** | PHP (بدون إطار عمل) | الباك إند الرئيسي REST API | +| **خادم المحفظة (Wallet Server)** | PHP (منفصل) | خدمة مالية مستقلة للمدفوعات | +| **خادم المواقع (Location Server)** | PHP (منفصل) | خدمة تتبع مواقع السائقين | +| **سيرفر السوكت (socket_intaleq)** | PHP WebSocket | خدمة الوقت الفعلي للتواصل المباشر | +| **خدمة OTP (flash-call-otp)** | Node.js | خدمة التحقق عبر المكالمات السريعة | +| **خدمة الخرائط (map-saas)** | خدمة منفصلة | خدمة التوجيه والبحث الجغرافي | + +### 1.2 النطاق الجغرافي + +النظام يعمل في عدة دول وأقاليم: +- **مصر** (القاهرة، الجيزة، الإسكندرية) +- **سوريا** +- **الأردن** (مسارات خاصة - routesjo) + +--- + +## 2. هيكل المشاريع والهندسة المعمارية + +### 2.1 الرسم البياني للهندسة المعمارية + +``` +┌──────────────────────────────────────────────────────────────┐ +│ تطبيقات الواجهة الأمامية │ +├─────────────┬──────────────┬──────────────┬──────────────────┤ +│ Intaleq │ intaleq_ │ intaleq_ │ service_ │ +│ (الركاب) │ driver │ admin │ intaleq │ +│ Flutter │ Flutter │ Flutter │ Flutter │ +└──────┬──────┴──────┬───────┴──────┬───────┴──────┬───────────┘ + │ │ │ │ + └─────────────┼──────────────┼──────────────┘ + │ │ + ┌──────▼──────────────▼──────┐ + │ API الرئيسية │ + │ api.intaleq.xyz │ + │ PHP REST (بدون إطار عمل) │ + │ + JWT Authentication │ + │ + Rate Limiting (Redis) │ + └──────┬─────────────────────┘ + │ + ┌────────────────┼─────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌──────────┐ ┌──────────────┐ ┌──────────────────┐ +│ MySQL │ │ Redis │ │ خوادم فرعية │ +│ intaleq │ │ (جلسات/تخزين │ │ - Wallet Server │ +│ DB1 │ │ مؤقت/Cache) │ │ - Location Server│ +└──────────┘ └──────────────┘ │ - Socket Server │ + │ - Map SaaS │ + └──────────────────┘ +``` + +### 2.2 هيكل مجلدات المشاريع + +#### intaleq_admin (لوحة الإدارة) +``` +lib/ +├── main.dart # نقطة الدخول +├── routes.dart # تعريف المسارات +├── binding/ +│ └── initial_binding.dart # ربط المتحكمات الأولية +├── constant/ # الثوابت +│ ├── api_key.dart # مفاتيح API سرية +│ ├── box_name.dart # مفاتيح التخزين المحلي +│ ├── colors.dart # ألوان التطبيق +│ ├── credential.dart # بيانات الاعتماد +│ ├── links.dart # روابط API (363 سطر - جميع نقاط النهاية) +│ ├── style.dart # أنماط التصميم +│ └── table_names.dart # أسماء الجداول +├── controller/ # طبقة المتحكمات +│ ├── auth/ # المصادقة +│ │ ├── login_controller.dart +│ │ └── otp_helper.dart +│ ├── admin/ # متحكمات لوحة الإدارة +│ │ ├── analytics_v2_controller.dart +│ │ ├── captain_admin_controller.dart +│ │ ├── complaint_controller.dart +│ │ ├── dashboard_controller.dart +│ │ ├── dashboard_v2_controller.dart +│ │ ├── driver_docs_controller.dart +│ │ ├── financial_v2_controller.dart +│ │ ├── get_all_invoice_controller.dart +│ │ ├── kazan_controller.dart +│ │ ├── passenger_admin_controller.dart +│ │ ├── promo_controller.dart +│ │ ├── quality_controller.dart +│ │ ├── register_captain_controller.dart +│ │ ├── ride_admin_controller.dart +│ │ ├── security_v2_controller.dart +│ │ ├── staff_controller.dart +│ │ ├── static_controller.dart +│ │ └── wallet_admin_controller.dart +│ ├── firebase/ # إشعارات Firebase +│ ├── functions/ # دوال مساعدة (CRUD، تشفير، AI) +│ └── bank_account/ # حسابات بنكية +├── models/ # نماذج البيانات +│ ├── db_sql.dart +│ └── model/ +│ └── admin/ # نماذج المشرفين +├── views/ # واجهات المستخدم +│ ├── admin/ # شاشات لوحة الإدارة +│ │ ├── admin_home_page.dart # الصفحة الرئيسية (1123 سطر) +│ │ ├── dashboard_v2_widget.dart +│ │ ├── captain/ # إدارة السائقين +│ │ ├── complaints/ # الشكاوى +│ │ ├── drivers/ # تتبع السائقين +│ │ ├── employee/ # الموظفون +│ │ ├── enceypt/ # أدوات التشفير +│ │ ├── financial/ # المالية +│ │ ├── passenger/ # الركاب +│ │ ├── pricing/ # التسعير +│ │ ├── promo/ # العروض الترويجية +│ │ ├── quality/ # الجودة +│ │ ├── rides/ # الرحلات +│ │ ├── security/ # الأمان +│ │ ├── server/ # مراقبة السيرفر +│ │ ├── staff/ # الكوادر +│ │ ├── static/ # الإحصائيات +│ │ └── wallet/ # المحفظة +│ ├── auth/ # شاشات المصادقة +│ └── widgets/ # مكونات قابلة لإعادة الاستخدام +└── services/ # (فارغ - لا توجد خدمات منفصلة) +``` + +--- + +## 3. تحليل الباك إند - الخادم الرئيسي + +### 3.1 التقنيات المستخدمة + +| التقنية | الوصف | +|---------|-------| +| **PHP** (بدون إطار عمل) | لغة السيرفر الأساسية - إصدار حديث مع `declare(strict_types=1)` | +| **MySQL** | قاعدة البيانات الرئيسية (intaleqDB1) | +| **Redis** | للتخزين المؤقت، إدارة الجلسات، Rate Limiting، والقوائم السوداء | +| **JWT** | للمصادقة والتوكنات | +| **PDO** | للاتصال الآمن بقاعدة البيانات | +| **Composer** | لإدارة المكتبات (Firebase JWT, Twilio, Stripe, PHPMailer) | +| **HMAC** | لتوقيع الطلبات المالية | + +### 3.2 هيكل الباك إند + +``` +intaleq_v1/ +├── connect.php # نقطة الدخول الموحدة (JWT مطلوب) +├── functions.php # دوال مساعدة (jsonSuccess, jsonError...) +├── login.php # تسجيل دخول API عام +├── loginAdmin.php # تسجيل دخول الأدمن (JWT) +├── core/ +│ ├── bootstrap.php # البوابة الرئيسية: Redis, Encryption, Headers +│ ├── helpers.php # دوال البيئة والمساعدة +│ ├── Auth/ +│ │ ├── JwtService.php # خدمة JWT كاملة +│ │ └── RateLimiter.php # تحديد معدل الطلبات +│ ├── Database/ +│ │ └── Database.php # اتصال قاعدة البيانات (Singleton) +│ ├── Security/ +│ │ └── EncryptionHelper.php # مساعد التشفير +│ └── Services/ # خدمات (فارغ حالياً) +├── Admin/ # نقاط نهاية لوحة الإدارة +│ ├── dashbord.php # لوحة المعلومات الرئيسية +│ ├── v2/ # خدمات الجيل الثاني +│ │ ├── realtime_dashboard.php +│ │ ├── smart_alerts.php +│ │ ├── analytics/ # التحليلات (نمو، إيرادات، ترتيب) +│ │ ├── financial/ # مالية (تسويات، إحصائيات) +│ │ ├── quality/ # جودة (قائمة سوداء، تقييم) +│ │ └── security/ # أمان (سجل التدقيق) +│ ├── adminUser/ # إدارة المشرفين +│ ├── driver/ # إدارة السائقين +│ ├── passenger/ # إدارة الركاب +│ ├── rides/ # إدارة الرحلات +│ ├── Staff/ # إدارة الكوادر +│ └── employee/ # الموظفون +├── auth/ # المصادقة +│ ├── captin/ # تسجيل/دخول السائقين +│ ├── sms/ # إرسال الرسائل النصية +│ ├── google_auth/ # مصادقة جوجل +│ ├── syria/ # مصادقة سوريا +│ └── passengerOTP/ # OTP الركاب +├── ride/ # عمليات الرحلات +│ ├── rides/ # إدارة الرحلات +│ ├── cancelRide/ # إلغاء الرحلات +│ ├── driver_order/ # طلبات السائقين +│ ├── kazan/ # التسعير +│ ├── promo/ # العروض الترويجية +│ ├── rate/ # التقييمات +│ ├── location/ # المواقع +│ ├── firebase/ # إشعارات Firebase +│ ├── driverWallet/ # محفظة السائقين +│ ├── passengerWallet/ # محفظة الركاب +│ ├── payment/ # المدفوعات +│ ├── notificationCaptain/ # إشعارات السائقين +│ ├── notificationPassenger/# إشعارات الركاب +│ ├── feedBack/ # التقييمات +│ ├── chat/ # المحادثة +│ ├── gamification/ # نظام التحفيز +│ └── invitor/ # نظام الدعوات +├── serviceapp/ # خدمة العملاء +│ ├── getComplaintAllData.php +│ ├── editCarPlate.php +│ ├── addNotesDriver.php +│ └── ... (20+ نقطة نهاية) +├── migration/ # الترحيل والتحديثات +├── EgyptDocuments/ # وثائق مصر +├── email/ # البريد الإلكتروني +├── webhook_sms/ # Webhook للرسائل +└── schema_*.sql # مخططات قواعد البيانات +``` + +### 3.3 نظام الأمان في الباك إند + +1. **JWT Authentication**: جميع الطلبات تتطلب توكن JWT صالح +2. **Rate Limiting**: عبر Redis لمنع الهجمات +3. **تشفير البيانات**: `EncryptionHelper` مع مفتاح 32 بايت +4. **HMAC للمعاملات المالية**: توقيع إضافي للخادم المالي +5. **CORS Headers**: مقيدة وآمنة +6. **Device Fingerprint**: تتبع الأجهزة عبر `X-Device-FP` + +### 3.4 نقاط القوة في الباك إند + +- ✅ بنية تحتية قوية مع Redis و JWT و Rate Limiting +- ✅ فصل الخدمات (Wallet, Location, Map SaaS مستقلة) +- ✅ نظام تحقق متعدد (OTP, Google, Flash Call) +- ✅ دعم متعدد الدول (مصر، سوريا، الأردن) +- ✅ نظام ترحيل (Migration) لتحديث قاعدة البيانات + +### 3.5 نقاط الضعف في الباك إند + +- ❌ **عدم استخدام إطار عمل**: PHP خام بدون Laravel/Symfony مما يصعب الصيانة +- ❌ **SQL مباشر**: استعلامات SQL مكتوبة يدوياً بدون ORM (عرضة للـ SQL Injection في بعض الأماكن) +- ❌ **غياب التوثيق**: لا يوجد API documentation (Swagger/OpenAPI) +- ❌ **غياب الاختبارات**: لا توجد Unit Tests أو Integration Tests +- ❌ **ملف connect.php المركزي**: نقطة فشل واحدة (Single Point of Failure) +- ❌ **كود مكرر**: دوال jsonSuccess/jsonError مكررة في أغلب الملفات +- ❌ **غياب logging منظم**: استخدام `error_log` فقط بدون مستويات + +--- + +## 4. تحليل لوحة الإدارة + +### 4.1 المميزات الحالية + +لوحة الإدارة توفر الوظائف التالية مقسمة حسب الفئات: + +#### 🧑‍🤝‍🧑 المستخدمين +- **الركاب**: عرض وإدارة وحذف وحظر +- **السائقون**: عرض وإدارة وتفعيل وحذف +- **المراقب**: تتبع السائقين مباشرة على الخريطة + +#### ⚙️ إدارة النظام +- **أكواد الخصم**: إنشاء وتعديل وحذف (Promo Management) +- **تعديل الأسعار**: تعديل نسب التسعير (Kazan Editor) +- **الشكاوى**: عرض وإدارة الشكاوى +- **مراجعة الوثائق**: فحص وثائق السائقين + +#### 📊 العمليات +- **الرحلات**: عرض وإدارة جميع الرحلات +- **مراقبة الرحلات**: متابعة الرحلات الجارية +- **الإحصائيات**: إحصائيات عامة +- **التحليلات المتقدمة**: تحليلات V2 + +#### 🛡️ الجودة والدعم +- **القائمة السوداء**: حظر سائقين/ركاب + +#### 💰 المالية والإدارة +- **الإدارة المالية V2**: لوحة مالية متقدمة +- **المحفظة**: إدارة المحافظ +- **هدية 300**: هدايا تحفيزية للسائقين +- **الفواتير**: إدارة الفواتير +- **الموظفون**: إدارة الموظفين +- **موافقة المشرفين**: قبول/رفض مشرفين جدد + +#### 🔧 النظام والتواصل +- **سجل العمليات**: سجل تدقيق أمني +- **واتساب جماعي**: رسائل جماعية للسائقين +- **إشعار سائقين/ركاب**: إشعارات Firebase +- **تسجيل سائق**: تسجيل سائقين جدد +- **تحديث التطبيق**: متابعة إصدارات التطبيق +- **مراقب السيرفر**: مراقبة حالة السيرفر +- **سجل الأخطاء**: عرض أخطاء النظام +- **أدوات التشفير**: تشفير بصمات الأجهزة +- **إدارة الكوادر**: إضافة مديرين وخدمة عملاء (للسوبر أدمن فقط) + +### 4.2 نظام الصلاحيات + +```dart +// التحقق من الصلاحيات في admin_home_page.dart +isSuperAdmin = (role == 'super_admin') || + (myPhone == '201023248456' || // أرقام محددة صلاحية مطلقة + myPhone == '963992952235' || + myPhone == '963942542053'); +``` + +**مستويات الصلاحيات:** +1. **super_admin**: صلاحية كاملة - رؤية المحفظة، إضافة كوادر، مراقبة الرحلات +2. **admin**: صلاحية محدودة - إدارة عامة بدون صلاحيات مالية حساسة +3. **service**: خدمة عملاء (غير مفعلة في كل الواجهات) + +### 4.3 نقاط القوة في لوحة الإدارة + +- ✅ **تصميم عصري وجميل**: استخدام ألوان داكنة مع تأثيرات بصرية جذابة +- ✅ **لوحة معلومات تفاعلية**: Dashboard V2 مع تحديث تلقائي كل دقيقتين +- ✅ **بحث سريع**: شريط بحث لتصفية الخدمات +- ✅ **تصنيف منظم**: تجميع الوظائف في فئات منطقية +- ✅ **دعم اللغة العربية**: الواجهة بالكامل بالعربية +- ✅ **تحديث تلقائي**: RefreshIndicator للسحب للتحديث + +### 4.4 نقاط الضعف في لوحة الإدارة + +- ❌ **ملف routes.dart غير مكتمل**: 7 مسارات فقط معرفة من أصل 30+ شاشة (الباقي عبر `Get.to()`) +- ❌ **كود كبير جداً**: `admin_home_page.dart` يحتوي 1123 سطر (يجب تقسيمه) +- ❌ **تكرار الكود**: نمط CRUD متكرر في أغلب المتحكمات +- ❌ **معالجة أخطاء غير متسقة**: بعض المتحكمات تستخدم try-catch والبعض لا +- ❌ **لا يوجد pagination**: جميع البيانات تُجلب دفعة واحدة +- ❌ **غياب التخزين المؤقت**: لا يوجد caching للبيانات محلياً +- ❌ **التحقق من الصلاحيات عبر أرقام هواتف ثابتة**: ممارسة غير آمنة +- ❌ **لا يوجد تسجيل خروج صريح**: إزالة التوكن فقط دون إلغاء في السيرفر +- ❌ **استخدام GetX لإدارة الحالة فقط**: بدون استخدام ميزات التنقل القوية في GetX +- ❌ **غياب WebSocket**: لا يوجد اتصال مباشر للتحديثات الفورية (باستثناء V2 timer) + +--- + +## 5. تحليل تطبيق خدمة العملاء + +### 5.1 الوظائف الرئيسية + +تطبيق `service_intaleq` هو أداة لموظفي خدمة العملاء للقيام بـ: +- تسجيل الدخول للموظفين +- استقبال إشعارات Firebase +- الوصول إلى وظائف serviceapp في الباك إند + +### 5.2 نقاط القوة +- ✅ دعم اللغات (العربية والإنجليزية) +- ✅ تكامل مع Firebase للإشعارات + +### 5.3 نقاط الضعف +- ❌ تطبيق بسيط جداً - يبدو غير مكتمل +- ❌ لا توجد شاشات واضحة للموظفين +- ❌ تكرار مع وظائف لوحة الإدارة + +--- + +## 6. قاعدة البيانات والموديلات + +### 6.1 الجداول الرئيسية (44+ جدول) + +#### جداول المستخدمين +| الجدول | الوصف | عدد الأعمدة | +|-------|-------|------------| +| `passengers` | الركاب | ~20 عمود | +| `driver` | السائقين | ~25 عمود | +| `adminUser` | المشرفين | 4 أعمدة | +| `employee` | الموظفين | 6 أعمدة | + +#### جداول الرحلات +| الجدول | الوصف | +|-------|-------| +| `ride` | الرحلات الرئيسية | +| `canecl` | إلغاءات الرحلات | +| `driver_orders` | طلبات السائقين | +| `driver_behavior` | سلوك السائقين | +| `driver_ride_scam` | احتيال الرحلات | + +#### جداول المركبات +| الجدول | الوصف | +|-------|-------| +| `CarRegistration` | تسجيل المركبات | +| `captains_car` | سيارات السائقين | +| `car_locations` | مواقع السيارات (Spatial Index) | +| `car_tracks` | تتبع مسار السيارات | + +#### جداول مالية +| الجدول | الوصف | +|-------|-------| +| `driverWallet` | محفظة السائقين | +| `driver_gifts` | هدايا السائقين | +| `invoice_records` | سجلات الفواتير | +| `invoicesAdmin` | فواتير المشرفين | +| `kazan` | التسعير | + +#### جداول النظام +| الجدول | الوصف | +|-------|-------| +| `complaint` | الشكاوى | +| `error` | سجل الأخطاء (115,338+ سجل) | +| `notifications` | إشعارات الركاب | +| `notificationCaptain` | إشعارات السائقين | +| `login_attempts` | محاولات الدخول | +| `api_keys` | مفاتيح API | + +### 6.2 نقاط القوة +- ✅ **تصميم جغرافي مكاني**: استخدام `POINT` و `SPATIAL INDEX` في جدول `car_locations` +- ✅ **تتبع كامل**: جداول لتتبع السلوك، المواقع، والمسار +- ✅ **أمان**: جداول لتسجيل المحاولات والأخطاء + +### 6.3 نقاط الضعف +- ❌ **عدم استخدام FK (Foreign Keys)**: العلاقات غير مقيدة +- ❌ **عدم تناسق التسمية**: camelCase مع snake_case في نفس الجدول +- ❌ **عدم وجود indexing كافٍ**: بعض الأعمدة المستخدمة بكثرة بدون INDEX +- ❌ **استخدام جداول قديمة**: مثل `captains_car` و `CarRegistration` المكررين +- ❌ **تخزين بيانات حساسة**: `password` غير مشفرة بشكل كافٍ +- ❌ **استخدام VARCHAR(6) لـ amount**: يجب أن يكون DECIMAL للمبالغ المالية + +--- + +## 7. نظام المصادقة والصلاحيات + +### 7.1 تدفق المصادقة + +``` +1. المستخدم ← رقم الهاتف + كلمة المرور +2. الباك إند ← التحقق + JWT (مع role) +3. التطبيق ← تخزين JWT مشفر في GetStorage +4. كل طلب ← إرسال JWT في Header Authorization +5. الباك إند ← فك JWT + استخراج role + التحقق +``` + +### 7.2 نظام التوكن + +```dart +// طريقة الحصول على JWT في CRUD +getJWT() async { + var payload = { + 'id': 'admin', + 'password': AK.passnpassenger, + 'aud': '${AK.allowed}$dev', + }; + // تشفير التوكن 3 مرات: + await box.write(BoxName.jwt, X.c(X.c(X.c(jwt, cn), cC), cs)); +} + +// فك التشفير عند الاستخدام: +token = r(rawJwt.toString()).split(AppInformation.addd)[0]; +``` + +**طبقات الأمان:** +1. **JWT** - توقيع رمزي +2. **تشفير 3 طبقات** - `X.c()` ثلاث مرات متتالية +3. **HMAC** - للمعاملات المالية +4. **Device Fingerprint** - `X-Device-FP` header + +### 7.3 نقاط القوة +- ✅ تشفير متعدد الطبقات للتوكنات +- ✅ نظام صلاحيات قائم على الأدوار (role-based) +- ✅ فصل مالي مع HMAC + +### 7.4 نقاط الضعف +- ❌ **أرقام هواتف ثابتة للسوبر أدمن**: ممارسة غير آمنة +- ❌ **عدم وجود انتهاء صلاحية فعال**: إعادة تلقائية دون تدخل المستخدم +- ❌ **تخزين التوكن محلياً**: بدون Secure Enclave +- ❌ **كلمة مرور ثابتة في الكود**: `AK.passnpassenger` +- ❌ **لا يوجد 2FA**: للمشرفين +- ❌ **لا يوجد audit log للتغييرات الحساسة** + +--- + +## 8. تحليل التوافقية بين الباك إند والأدمن + +### 8.1 مصفوفة التوافق + +| الوظيفة | الباك إند | الأدمن | الحالة | +|--------|----------|--------|--------| +| لوحة المعلومات | `Admin/dashbord.php` | `DashboardController` | ✅ متوافق | +| لوحة V2 | `Admin/v2/realtime_dashboard.php` | `DashboardV2Controller` | ✅ متوافق | +| قائمة السائقين | `Admin/AdminCaptain/get.php` | `captain_admin_controller.dart` | ✅ متوافق | +| تفاصيل السائق | `auth/syria/driver/driver_details.php` | `captain_details.dart` | ✅ متوافق | +| الركاب | `Admin/getPassengerDetails.php` | `passenger_admin_controller.dart` | ✅ متوافق | +| أكواد الخصم | `ride/promo/` | `promo_controller.dart` | ✅ متوافق | +| التسعير | `ride/kazan/` | `kazan_controller.dart` | ✅ متوافق | +| الشكاوى | `serviceapp/getComplaintAllData.php` | `complaint_controller.dart` | ✅ متوافق | +| المالية V2 | `v2/financial/` | `financial_v2_controller.dart` | ✅ متوافق | +| سجل التدقيق | `v2/security/audit_logs.php` | `security_v2_controller.dart` | ✅ متوافق | +| الإحصائيات | `Admin/getRidesPerMonth.php` | `static_controller.dart` | ✅ متوافق | +| المحادثة | `ride/chat/` | ❌ غير موجود | ❌ غير متوافق | +| نظام التحفيز | `ride/gamification/` | ❌ غير موجود | ❌ غير متوافق | +| تتبع حي | WebSocket | ❌ غير موجود | ❌ غير متوافق | + +### 8.2 مشاكل التوافقية + +1. **ملف routes.dart غير مكتمل**: 7 مسارات فقط من أصل 30+ وظيفة تستخدم `Get.toNamed()` +2. **استخدام `Get.to()` المباشر**: بدون تعريفها في routes، مما يصعب تتبع التنقل +3. **اختلاف أسماء الحقول**: بعض الـ payload تختلف تسميتها بين الأدمن والباك إند +4. **غياب معالجة Versioning**: لا توجد آلية لتحديد إصدار API +5. **نقاط نهاية غير مستخدمة**: بعض endpoints في الباك إند لا تقابلها شاشات في الأدمن + +--- + +## 9. الإضافات والتحسينات المقترحة + +### 9.1 تحسينات أمنية (أولوية عالية) 🔴 + +1. **إضافة 2FA للمشرفين** + - إضافة مصادقة ثنائية عبر Google Authenticator أو OTP + - حماية الوصول للوحة الإدارة + +2. **تحسين نظام الصلاحيات** + - استبدال أرقام الهواتف الثابتة بنظام صلاحيات ديناميكي + - إضافة صلاحيات دقيقة (Granular Permissions) بدلاً من admin/super_admin فقط + +3. **تسجيل الخروج من جميع الأجهزة** + - إضافة `token_blacklist` في Redis + - إمكانية إنهاء جلسات محددة + +4. **تشفير البيانات الحساسة** + - نقل `api_key.dart` إلى متغيرات بيئة آمنة + - استخدام Secure Enclave لتخزين التوكنات + +### 9.2 تحسينات معمارية (أولوية متوسطة) 🟡 + +5. **إعادة هيكلة routes.dart** + - تعريف جميع المسارات (~30 مسار) في ملف routes + - استخدام `GetPage` مع `binding` و `middleware` + +6. **تقسيم admin_home_page.dart** + - فصل إلى مكونات مستقلة (StatelessWidget) + - نقل كل قسم إلى ملف منفصل + +7. **طبقة خدمة موحدة (Service Layer)** + - إنشاء BaseRepository/BaseService + - توحيد معالجة الـ CRUD + - إضافة retry policy و offline support + +8. **استخدام Riverpod/Bloc بدلاً من GetX** + - GetX يعمل ولكن ليس الخيار الأمثل للتطبيقات الكبيرة + - تحسين فصل المسؤوليات (Separation of Concerns) + +### 9.3 تحسينات وظيفية (أولوية متوسطة) 🟡 + +9. **لوحة معلومات قابلة للتخصيص** + - إضافة Widgets قابلة للسحب والإفلات + - حفظ تفضيلات المستخدم + +10. **تقارير PDF مصدرة** + - تصدير الإحصائيات كـ PDF + - جداولة التقارير الدورية + +11. **نظام تنبيهات ذكي** + - تنبيهات تلقائية عند تجاوز الحدود (عدد شكاوى، إلغاءات) + - Machine Learning للكشف عن السلوك الاحتيالي + +12. **محادثة مباشرة مع السائقين والركاب** + - دمج chat داخل لوحة الإدارة + - استخدام WebSocket للتواصل الفوري + +### 9.4 تحسينات تقنية (أولوية منخفضة) 🟢 + +13. **Pagination و Infinite Scroll** + - لجميع القوائم (السائقين، الركاب، الرحلات) + +14. **WebSocket للوحة الإدارة** + - اتصال مباشر مع socket_intaleq + - تحديثات فورية بدون polling + +15. **دعم الوضع المظلم/الفاتح** + - إضافة theme toggle + - دعم system theme + +16. **اختبارات واجهة المستخدم** + - Widget Tests و Integration Tests + +### 9.5 إضافات جديدة مقترحة (أولوية متنوعة) 💡 + +17. **لوحة تحليلات الأعمال (BI Dashboard)** + - رسوم بيانية تفاعلية + - مقارنة الأداء بين الفترات + - توقع الإيرادات المستقبلية + +18. **نظام إدارة المناطق الجغرافية** + - رسم المناطق النشطة على الخريطة + - تحليل الكثافة والتغطية + - إدارة المناطق المحظورة + +19. **نظام Gamification Dashboard** + - لوحة كاملة لنظام التحفيز (موجود API بدون UI) + - عرض تصنيفات السائقين + - إدارة المكافآت والتحديات + +20. **نظام إدارة الحملات التسويقية** + - إنشاء حملات إشعارات مستهدفة + - A/B Testing للإشعارات + - قياس فعالية الحملات + +21. **تكامل مع منصات خارجية** + - تصدير البيانات إلى Google Sheets + - Slack/Telegram notifications للإشعارات المهمة + - Webhook للمطورين الخارجيين + +22. **نظام توثيق API تلقائي** + - Swagger/OpenAPI documentation + - Sandbox environment للاختبار + +--- + +## 10. ملخص وتوصيات + +### 10.1 ملخص الحالة الراهنة + +نظام إنتلق هو نظام متكامل وقوي لتطبيقات نقل الركاب، مع: +- **بنية تحتية متقدمة**: Redis، JWT، HMAC، Rate Limiting +- **مجموعة واسعة من الميزات**: إدارة ركاب، سائقين، رحلات، مالية، شكاوى، إشعارات +- **دعم متعدد الدول**: مصر، سوريا، الأردن مع إمكانية التوسع +- **تصميم عصري**: واجهة مستخدم جذابة باللغة العربية + +**نقاط القوة الرئيسية:** +1. نظام مصادقة آمن مع JWT و HMAC متعدد الطبقات +2. فصل الخدمات المالية والجغرافية في خوادم مستقلة +3. دعم التتبع الجغرافي المكاني (Spatial Index) +4. نظام V2 للوحة المعلومات بتحديث تلقائي +5. واجهة إدارة شاملة تغطي معظم جوانب النظام + +**نقاط الضعف الرئيسية:** +1. عدم استخدام إطار عمل PHP (صعوبة في الصيانة والتوسع) +2. كود كبير في ملفات مفردة (1123 سطر في admin_home_page.dart) +3. نظام صلاحيات بدائي مع أرقام هواتف ثابتة +4. نقص التوثيق والاختبارات +5. طرق تنقل غير متسقة في التطبيق (مزيج بين Get.toNamed و Get.to) + +### 10.2 خريطة الطريق المقترحة + +``` +المرحلة الأولى (شهر 1-2) - تحسينات أمنية وحرجة: +├── 2FA للمشرفين +├── تحسين نظام الصلاحيات +├── نقل المفاتيح السرية إلى بيئة آمنة +└── إصلاح الثغرات الأمنية + +المرحلة الثانية (شهر 3-4) - تحسينات معمارية: +├── إعادة هيكلة routes +├── تقسيم الملفات الكبيرة +├── إضافة طبقة خدمة موحدة +└── Pagination لكل القوائم + +المرحلة الثالثة (شهر 5-6) - إضافات جديدة: +├── لوحة BI Dashboard +├── WebSocket Integration +├── تقارير PDF +└── نظام إدارة المناطق + +المرحلة الرابعة (شهر 7-8) - تحسينات متقدمة: +├── API Documentation (Swagger) +├── اختبارات آلية +├── Offline Support +└── تكامل مع منصات خارجية +``` + +### 10.3 توصيات فورية + +1. **🚨 عاجل**: نقل `api_key.dart` إلى متغيرات بيئة (`.env`) وليس في الكود +2. **🚨 عاجل**: إزالة أرقام الهواتف الثابتة من كود التحقق من الصلاحيات +3. **⚠️ مهم**: إكمال ملف `routes.dart` بجميع المسارات +4. **⚠️ مهم**: إضافة `INDEX` للأعمدة المستخدمة بكثرة في قاعدة البيانات +5. **📝 موصى به**: إنشاء `README.md` شامل لكل مشروع + +--- + +
+ +--- + +**تم إعداد هذا التقرير بواسطة التحليل الآلي للنظام** + +*تاريخ الإعداد: 6 يناير 2026* + +*الإصدار: 1.0* + +
+ +
\ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index a14b677..54a9d05 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -289,7 +289,7 @@ packages: source: hosted version: "0.3.5+2" crypto: - dependency: transitive + dependency: "direct main" description: name: crypto sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf @@ -320,6 +320,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.6" + dart_webrtc: + dependency: transitive + description: + name: dart_webrtc + sha256: f6d615bddea5e458ce180a914f3055c234ffb52fb7397a51b3491e76d6d7edb2 + url: "https://pub.dev" + source: hosted + version: "1.8.1" dbus: dependency: transitive description: @@ -742,6 +750,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_webrtc: + dependency: "direct main" + description: + name: flutter_webrtc + sha256: c7b0a67ca2c878575fc5c146d801cd874f58f5f1ef5fa6e8eb0c93d413beb948 + url: "https://pub.dev" + source: hosted + version: "1.4.1" flutter_widget_from_html: dependency: "direct main" description: @@ -1324,6 +1340,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.1" + logger: + dependency: transitive + description: + name: logger + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.dev" + source: hosted + version: "2.7.0" logging: dependency: transitive description: @@ -2224,6 +2248,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + webrtc_interface: + dependency: transitive + description: + name: webrtc_interface + sha256: c6f100eac5057d9a817a60473126f9828c796d42884d498af4f339c97b21014f + url: "https://pub.dev" + source: hosted + version: "1.5.1" webview_flutter: dependency: "direct main" description: @@ -2233,7 +2265,7 @@ packages: source: hosted version: "4.9.0" webview_flutter_android: - dependency: transitive + dependency: "direct main" description: name: webview_flutter_android sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" @@ -2249,7 +2281,7 @@ packages: source: hosted version: "2.14.0" webview_flutter_wkwebview: - dependency: transitive + dependency: "direct main" description: name: webview_flutter_wkwebview sha256: "108bd85d0ff20bff1e8b52a040f5c19b6b9fc4a78fdf3160534ff5a11a82e267" diff --git a/pubspec.yaml b/pubspec.yaml index 1bdbb62..be0cf17 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.8 + flutter_webrtc: ^1.4.1 secure_string_operations: path: ./secure_string_operations firebase_messaging: ^16.1.1 @@ -19,6 +20,7 @@ dependencies: path: ^1.9.1 intl: ^0.20.2 http: ^1.2.2 + crypto: ^3.0.3 get: path: ./packages/get get_storage: @@ -56,6 +58,8 @@ dependencies: record: ^6.2.0 dio: ^5.9.1 webview_flutter: ^4.9.0 + webview_flutter_android: ^3.16.2 + webview_flutter_wkwebview: ^3.14.0 just_audio: ^0.10.5 # share: ^2.0.4 google_sign_in: ^7.2.0 diff --git a/scratch/benchmark_route.py b/scratch/benchmark_route.py new file mode 100644 index 0000000..e33bf73 --- /dev/null +++ b/scratch/benchmark_route.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 +import time +import urllib.request +import urllib.error +import json +import random +from concurrent.futures import ThreadPoolExecutor, as_completed + +# ==================== CONFIGURATION ==================== +API_KEY = "" # Kept your actual API key +BASE_URL = "https://map-saas.intaleqapp.com/api/maps/route" +CONCURRENCY = 1000 # Number of concurrent threads +TOTAL_REQUESTS = 10000 # Total number of requests to send +TIMEOUT_SECONDS = 10 # Request timeout + +# Bounding boxes for heavily populated regions inside Jordan and Syria (excluding deserts and Egypt since it is not in the map database) +REGIONS = { + "Amman (Jordan)": { + "lat_min": 31.85, "lat_max": 32.15, + "lng_min": 35.80, "lng_max": 36.00 + }, + "Irbid (Jordan)": { + "lat_min": 32.45, "lat_max": 32.60, + "lng_min": 35.80, "lng_max": 35.95 + }, + "Damascus (Syria)": { + "lat_min": 33.45, "lat_max": 33.55, + "lng_min": 36.25, "lng_max": 36.35 + }, + "Aleppo (Syria)": { + "lat_min": 36.15, "lat_max": 36.25, + "lng_min": 37.10, "lng_max": 37.20 + } +} +# ======================================================= + +def generate_random_route(): + """Generates random starting and ending points inside Jordan and Syria populated cities.""" + region_name = random.choice(list(REGIONS.keys())) + bbox = REGIONS[region_name] + + # Pick a random starting point in the selected region + from_lat = random.uniform(bbox["lat_min"], bbox["lat_max"]) + from_lng = random.uniform(bbox["lng_min"], bbox["lng_max"]) + + # Pick a destination within the same region (~10-15km max) to ensure a quick and valid route + to_lat = from_lat + random.uniform(-0.08, 0.08) + to_lng = from_lng + random.uniform(-0.08, 0.08) + + # Clip coordinates to region bounds + to_lat = max(bbox["lat_min"], min(to_lat, bbox["lat_max"])) + to_lng = max(bbox["lng_min"], min(to_lng, bbox["lng_max"])) + + return region_name, from_lat, from_lng, to_lat, to_lng + +def send_request(request_id): + region_name, from_lat, from_lng, to_lat, to_lng = generate_random_route() + + # Construct dynamic URL with random coordinates + url = ( + f"{BASE_URL}?fromLat={from_lat:.5f}&fromLng={from_lng:.5f}" + f"&toLat={to_lat:.5f}&toLng={to_lng:.5f}&locale=ar" + ) + + req = urllib.request.Request( + url, + headers={ + "x-api-key": API_KEY, + "User-Agent": "Benchmark-Client/1.0" + } + ) + + start_time = time.perf_counter() + status_code = 0 + error_message = None + + try: + with urllib.request.urlopen(req, timeout=TIMEOUT_SECONDS) as response: + status_code = response.status + response.read() + except urllib.error.HTTPError as e: + status_code = e.code + try: + err_body = e.read().decode('utf-8') + error_message = f"HTTP {e.code}: {json.loads(err_body).get('message', e.reason)}" + except Exception: + error_message = f"HTTP Error {e.code}: {e.reason}" + except urllib.error.URLError as e: + status_code = 0 + error_message = f"URL Error: {e.reason}" + except Exception as e: + status_code = 0 + error_message = f"Generic Error: {str(e)}" + + end_time = time.perf_counter() + latency = (end_time - start_time) * 1000.0 # Convert to milliseconds + + return { + "id": request_id, + "region": region_name, + "success": 200 <= status_code < 300, + "status_code": status_code, + "latency": latency, + "error": error_message + } + +def print_report(results, elapsed_time): + latencies = [r["latency"] for r in results] + successes = [r for r in results if r["success"]] + failures = [r for r in results if not r["success"]] + + latencies.sort() + + total_reqs = len(results) + success_count = len(successes) + failure_count = len(failures) + + avg_latency = sum(latencies) / total_reqs if total_reqs > 0 else 0 + min_latency = latencies[0] if latencies else 0 + max_latency = latencies[-1] if latencies else 0 + + def percentile(p): + if not latencies: + return 0 + idx = int(len(latencies) * p) + return latencies[min(idx, len(latencies) - 1)] + + rps = total_reqs / elapsed_time if elapsed_time > 0 else 0 + + # Calculate stats per region + region_stats = {} + for r in results: + reg = r["region"] + if reg not in region_stats: + region_stats[reg] = {"total": 0, "success": 0, "latencies": []} + region_stats[reg]["total"] += 1 + if r["success"]: + region_stats[reg]["success"] += 1 + region_stats[reg]["latencies"].append(r["latency"]) + + print("\n" + "="*50) + print(" API LOAD TESTING REPORT ") + print("="*50) + print(f"Target URL: {BASE_URL}") + print(f"Concurrency Level: {CONCURRENCY} threads") + print(f"Total Requests: {total_reqs}") + print(f"Time Taken: {elapsed_time:.3f} seconds") + print(f"Successful Requests: {success_count} ({success_count/total_reqs*100:.1f}%)") + print(f"Failed Requests: {failure_count} ({failure_count/total_reqs*100:.1f}%)") + print(f"Requests per Second: {rps:.2f} RPS") + print("-"*50) + print("PER-REGION SUMMARY:") + for region, stats in region_stats.items(): + r_avg = sum(stats["latencies"]) / stats["total"] if stats["total"] > 0 else 0 + print(f" {region:17}: {stats['total']} reqs, Avg: {r_avg:.1f}ms, Success: {stats['success']}/{stats['total']}") + print("-"*50) + print("LATENCY STATISTICS (ms):") + print(f" Min: {min_latency:.2f} ms") + print(f" Max: {max_latency:.2f} ms") + print(f" Average: {avg_latency:.2f} ms") + print(f" Median (50%): {percentile(0.50):.2f} ms") + print(f" 90th Percentile: {percentile(0.90):.2f} ms") + print(f" 95th Percentile: {percentile(0.95):.2f} ms") + print(f" 99th Percentile: {percentile(0.99):.2f} ms") + print("="*50) + + if failure_count > 0: + print("\nERROR SUMMARY:") + errors = {} + for f in failures: + err = f["error"] or f"HTTP {f['status_code']}" + errors[err] = errors.get(err, 0) + 1 + for err, count in errors.items(): + print(f" - {err}: {count} occurrence(s)") + print("="*50) + +def main(): + print(f"Starting dynamic benchmark of: {BASE_URL}") + print(f"Sending {TOTAL_REQUESTS} randomized requests (Amman, Irbid, Damascus, Aleppo)...") + print(f"Concurrency Level: {CONCURRENCY} concurrent threads") + + results = [] + start_time = time.perf_counter() + + with ThreadPoolExecutor(max_workers=CONCURRENCY) as executor: + futures = {executor.submit(send_request, i): i for i in range(TOTAL_REQUESTS)} + + completed = 0 + for future in as_completed(futures): + results.append(future.result()) + completed += 1 + if completed % (TOTAL_REQUESTS // 10 or 1) == 0 or completed == TOTAL_REQUESTS: + print(f"Progress: {completed}/{TOTAL_REQUESTS} requests completed...") + + end_time = time.perf_counter() + elapsed_time = end_time - start_time + + print_report(results, elapsed_time) + +if __name__ == "__main__": + main() diff --git a/scratch/test_api.py b/scratch/test_api.py new file mode 100644 index 0000000..9906099 --- /dev/null +++ b/scratch/test_api.py @@ -0,0 +1,17 @@ +import urllib.request +import json + +def check_country(country): + url = "https://api.intaleq.xyz/intaleq_v3/ride/kazan/get.php" + data = json.dumps({"country": country}).encode('utf-8') + req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'}) + try: + with urllib.request.urlopen(req) as response: + res = response.read().decode('utf-8') + print(f"Country: {country} -> {res}") + except Exception as e: + print(f"Country: {country} -> Error: {e}") + +check_country("Jordan") +check_country("Syria") +check_country("Egypt")