feat: refactor financial wallet UI components and add offline map service support

This commit is contained in:
Hamza-Ayed
2026-04-21 00:35:30 +03:00
parent 4293d20561
commit b92db3bb39
99 changed files with 22888 additions and 27387 deletions

View File

@@ -5,28 +5,21 @@ import 'dart:math';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:sefer_driver/controller/firebase/local_notification.dart';
import 'package:sefer_driver/controller/home/captin/behavior_controller.dart';
import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart';
import 'package:sefer_driver/controller/home/navigation/decode_polyline_isolate.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'package:bubble_head/bubble.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_polyline_algorithm/google_polyline_algorithm.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../constant/api_key.dart';
import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../constant/country_polygons.dart';
import '../../../constant/links.dart';
import '../../../constant/table_names.dart';
import '../../../env/env.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../../views/Rate/rate_passenger.dart';
@@ -36,6 +29,7 @@ import '../../firebase/notification_service.dart';
import '../../functions/crud.dart';
import '../../functions/location_controller.dart';
import '../../functions/tts.dart';
import 'behavior_controller.dart';
class MapDriverController extends GetxController {
bool isLoading = true;
@@ -48,10 +42,10 @@ class MapDriverController extends GetxController {
List data = [];
List dataDestination = [];
LatLngBounds? boundsData;
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
BitmapDescriptor passengerIcon = BitmapDescriptor.defaultMarker;
BitmapDescriptor startIcon = BitmapDescriptor.defaultMarker;
BitmapDescriptor endIcon = BitmapDescriptor.defaultMarker;
InlqBitmap carIcon = InlqBitmap.defaultMarker;
InlqBitmap passengerIcon = InlqBitmap.defaultMarker;
InlqBitmap startIcon = InlqBitmap.defaultMarker;
InlqBitmap endIcon = InlqBitmap.defaultMarker;
final List<LatLng> polylineCoordinates = [];
final List<LatLng> polylineCoordinatesDestination = [];
List<Polyline> polyLines = [];
@@ -104,7 +98,7 @@ class MapDriverController extends GetxController {
int remainingTimeToPassenger = 60;
int remainingTimeInPassengerLocatioWait = 60;
bool isDriverNearPassengerStart = false;
GoogleMapController? mapController;
IntaleqMapController? mapController;
late LatLng myLocation;
int remainingTimeTimerRideBegin = 60;
String stringRemainingTimeRideBegin = '';
@@ -158,16 +152,32 @@ class MapDriverController extends GetxController {
_posSub?.cancel();
_posSub = null;
mapController?.dispose();
// mapController?.dispose();
super.onClose();
}
void onMapCreated(GoogleMapController controller) {
void onMapCreated(IntaleqMapController controller) {
mapController = controller;
if (Get.isRegistered<LocationController>()) {
myLocation = Get.find<LocationController>().myLocation;
controller.animateCamera(CameraUpdate.newLatLngZoom(myLocation, 16));
}
// 🔥 رسم المسار فور جاهزية الخريطة
if (isRideStarted) {
// إذا كانت الرحلة بدأت، ارسم للمكان النهائي
getRoute(
origin: myLocation,
destination: latLngPassengerDestination,
routeColor: Colors.blue);
} else {
// إذا كان السائق ذاهب للراكب
getRoute(
origin: myLocation,
destination: latLngPassengerLocation,
routeColor: Colors.yellow);
}
// بدء الاستماع للموقع للملاحة وتحديث الماركر
startListeningStepNavigation();
}
@@ -239,7 +249,7 @@ class MapDriverController extends GetxController {
}
takeSnapMap() {
mapController!.takeSnapshot();
// mapController!.takeSnapshot();
}
@override
@@ -254,7 +264,7 @@ class MapDriverController extends GetxController {
_passengerTimer?.cancel();
_waitingTimer?.cancel();
_posSub?.cancel();
mapController?.dispose();
// mapController?.dispose();
}
Future openGoogleMapFromDriverToPassenger() async {
@@ -307,7 +317,9 @@ class MapDriverController extends GetxController {
box.remove(BoxName.rideArguments);
// 3. عرض رسالة للسائق
if (Get.isDialogOpen == true) Get.back(); // إغلاق أي ديالوج مفتوح
if (Get.isDialogOpen == true) {
navigatorKey.currentState?.pop();
}
Get.defaultDialog(
title: "تم إلغاء الرحلة".tr,
@@ -325,7 +337,7 @@ class MapDriverController extends GetxController {
),
confirm: ElevatedButton(
onPressed: () {
Get.back(); // إغلاق الديالوج
navigatorKey.currentState?.pop(); // إغلاق الديالوج
Get.offAll(() => HomeCaptain()); // العودة للرئيسية
},
child: Text("OK".tr),
@@ -367,8 +379,8 @@ class MapDriverController extends GetxController {
box.write(BoxName.statusDriverLocation, 'blocked');
// عرض رسالة العقوبة
Get.snackbar("تم تقييد حسابك مؤقتاً ⛔",
"بسبب كثرة الإلغاءات (3 مرات)، تم إيقاف استقبال الطلبات لمدة 4 ساعات.",
Get.snackbar("Your account is temporarily restricted ⛔".tr,
"Due to excessive cancellations (3 times), receiving orders has been suspended for 4 hours.".tr,
duration: Duration(seconds: 8),
backgroundColor: Colors.red,
colorText: Colors.white,
@@ -402,15 +414,21 @@ class MapDriverController extends GetxController {
Get.put(HomeCaptainController()).getRefusedOrderByCaptain();
}
if (Get.isDialogOpen == true) Get.back();
if (Get.isDialogOpen == true) {
navigatorKey.currentState?.pop();
}
Get.offAll(
() => HomeCaptain()); // العودة للرئيسية ليتم تطبيق الحظر هناك
} else {
if (Get.isDialogOpen == true) Get.back();
if (Get.isDialogOpen == true) {
navigatorKey.currentState?.pop();
}
Get.snackbar("Error", "Failed to cancel ride");
}
} catch (e) {
if (Get.isDialogOpen == true) Get.back();
if (Get.isDialogOpen == true) {
navigatorKey.currentState?.pop();
}
Log.print("Error: $e");
}
}
@@ -707,7 +725,9 @@ class MapDriverController extends GetxController {
await calculateDistanceBetweenDriverAndPassengerLocation();
// إغلاق مؤشر التحميل لأننا حصلنا على النتيجة
if (Get.isDialogOpen == true) Get.back();
if (Get.isDialogOpen == true) {
navigatorKey.currentState?.pop();
}
if (distanceToPassenger < 100) {
// زدت المسافة قليلاً لمرونة أكبر (150م)
@@ -752,15 +772,17 @@ class MapDriverController extends GetxController {
});
} else {
// --- حالة الرفض (بعيد جداً) ---
MyDialog().getDialog(
'You are far from passenger location'.tr,
MyDialog().getDialog('You are far from passenger location'.tr,
'Please go closer to the passenger location (less than 150m)'.tr,
() => Get.back() // إغلاق الديالوج فقط
);
() {
// الديالوج يغلق نفسه الآن تلقائياً
});
}
} catch (e) {
// تنظيف اللودينج في حال حدوث خطأ غير متوقع
if (Get.isDialogOpen == true) Get.back();
if (Get.isDialogOpen == true) {
navigatorKey.currentState?.pop();
}
Log.print("Error starting ride: $e");
Get.snackbar("Error", "Could not start ride. Please check internet.");
}
@@ -1448,56 +1470,23 @@ class MapDriverController extends GetxController {
}
void addCustomCarIcon() {
ImageConfiguration config = ImageConfiguration(
size: const Size(30, 35), devicePixelRatio: Get.pixelRatio);
BitmapDescriptor.asset(
config,
'assets/images/car.png',
// mipmaps: false,
).then((value) {
carIcon = value;
update();
});
carIcon = InlqBitmap.fromAsset('assets/images/car.png');
update();
}
void addCustomStartIcon() async {
// Create the marker with the resized image
ImageConfiguration config = ImageConfiguration(
size: const Size(30, 30), devicePixelRatio: Get.pixelRatio);
BitmapDescriptor.asset(
config,
'assets/images/A.png',
).then((value) {
startIcon = value;
update();
});
startIcon = InlqBitmap.fromAsset('assets/images/A.png');
update();
}
void addCustomEndIcon() {
ImageConfiguration config = ImageConfiguration(
size: const Size(25, 25), devicePixelRatio: Get.pixelRatio);
BitmapDescriptor.asset(
config,
'assets/images/b.png',
).then((value) {
endIcon = value;
update();
});
endIcon = InlqBitmap.fromAsset('assets/images/b.png');
update();
}
void addCustomPassengerIcon() {
ImageConfiguration config = ImageConfiguration(
size: const Size(30, 30), devicePixelRatio: Get.pixelRatio
// scale: 1.0,
);
BitmapDescriptor.asset(
config,
'assets/images/picker.png',
).then((value) {
passengerIcon = value;
update();
});
passengerIcon = InlqBitmap.fromAsset('assets/images/picker.png');
update();
}
var activeRouteSteps = <Map<String, dynamic>>[];
@@ -1596,82 +1585,94 @@ class MapDriverController extends GetxController {
required LatLng destination,
required Color routeColor,
}) async {
// 1. استخدام الرابط الجديد والإعدادات الصحيحة
String coordinates =
'${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}';
// استخدام الرابط من الكلاس المرجعي لأنه أحدث
var url =
"${AppLink.mapOSM}/route/v1/driving/$coordinates?steps=true&overview=full";
if (mapController == null) return;
try {
var response = await http.get(Uri.parse(url));
// 1. طلب المسار من الباكيج
final response =
await mapController!.getDirections(origin, destination, steps: true);
if (response.statusCode == 200) {
var decoded = jsonDecode(response.body);
// 2. التعامل مع الـ JSON المباشر (الذي أرسله المستخدم)
// إذا كان الـ response يحتوي على الحقول مباشرة في الجذر
final String? encodedPoints = response['points'];
if (decoded['code'] != 'Ok' || (decoded['routes'] as List).isEmpty) {
mySnackeBarError("لم يتم العثور على مسار");
return;
}
if (encodedPoints == null) {
mySnackeBarError("No route points found".tr);
return;
}
var route = decoded['routes'][0];
// 🔥 فك التشفير باستخدام compute لضمان أداء ممتاز
List<LatLng> fullRoute =
await compute(PolylineUtils.decode, encodedPoints);
// أ) تشغيل الـ Isolate لفك التشفير (ممتاز، ابقِ عليه)
final String pointsString = route["geometry"];
List<LatLng> fullRoute =
await compute(decodePolylineIsolate, pointsString);
if (fullRoute.isEmpty) {
mySnackeBarError("Failed to process route points".tr);
return;
}
// ب) تهيئة المتغيرات
upcomingPathPoints.assignAll(fullRoute);
traveledPathPoints.clear();
_lastTraveledIndex = 0;
// تحديث المسافة والوقت من الـ JSON الجديد
distance = (response['distance'] ?? 0).toString();
duration = (response['duration'] ?? 0).toString();
// ج) رسم المسار الأولي
polyLines.clear();
polyLines.add(Polyline(
polylineId: const PolylineId("upcoming_route"),
points: fullRoute,
width: 8,
color: routeColor,
startCap: Cap.roundCap,
endCap: Cap.roundCap,
));
// ب) تهيئة المتغيرات
upcomingPathPoints.assignAll(fullRoute);
traveledPathPoints.clear();
_lastTraveledIndex = 0;
// د) معالجة الخطوات (Legs & Steps)
List<dynamic> legs = route['legs'];
if (legs.isNotEmpty) {
final stepsList = List<Map<String, dynamic>>.from(legs[0]['steps']);
// ج) رسم المسار الأولي
polyLines.clear();
polyLines.add(Polyline(
polylineId: const PolylineId("upcoming_route"),
points: fullRoute,
width: 8,
color: routeColor,
));
// 🔥 استخدام دالة الترجمة المحسنة من الكلاس المرجعي
for (var step in stepsList) {
step['html_instructions'] =
_createInstructionFromManeuverSmart(step);
// تصحيح مواقع المناورات
if (step['maneuver'] != null &&
step['maneuver']['location'] != null) {
var loc = step['maneuver']['location'];
// د) معالجة الخطوات (Legs & Steps) إذا توفرت في الـ JSON
List<dynamic> legs = response['legs'] ?? [];
if (legs.isNotEmpty) {
final stepsList =
List<Map<String, dynamic>>.from(legs[0]['steps'] ?? []);
for (var step in stepsList) {
step['html_instructions'] = _createInstructionFromManeuverSmart(step);
if (step['maneuver'] != null &&
step['maneuver']['location'] != null) {
var loc = step['maneuver']['location'];
// التعامل مع تنسيق OSRM [lng, lat]
if (loc is List && loc.length >= 2) {
step['end_location'] = {'lat': loc[1], 'lng': loc[0]};
} else if (loc is Map) {
step['end_location'] = loc;
}
}
}
routeSteps = stepsList;
currentStepIndex = 0;
routeSteps = stepsList;
currentStepIndex = 0;
// نطق أول تعليمة
if (routeSteps.isNotEmpty) {
currentInstruction = routeSteps[0]['html_instructions'];
// نطق أول تعليمة
if (routeSteps.isNotEmpty) {
currentInstruction = routeSteps[0]['html_instructions'];
if (Get.isRegistered<TextToSpeechController>()) {
Get.find<TextToSpeechController>().speakText(currentInstruction);
}
}
// هـ) تحريك الكاميرا لتشمل المسار
if (fullRoute.isNotEmpty) {
final bounds = _boundsFromLatLngList(fullRoute);
safeAnimateCamera(CameraUpdate.newLatLngBounds(bounds, 80));
}
update();
} else {
// في حال عدم وجود steps، نقوم بتصفيرها
routeSteps = [];
currentInstruction = "";
}
// هـ) تحريك الكاميرا لتشمل المسار
if (fullRoute.isNotEmpty) {
final bounds = _boundsFromLatLngList(fullRoute);
safeAnimateCamera(CameraUpdate.newLatLngBounds(bounds,
left: 80, top: 80, right: 80, bottom: 80));
}
update();
} catch (e) {
Log.print("Route Error: $e");
}
@@ -1679,7 +1680,7 @@ class MapDriverController extends GetxController {
// 🔥 دالة الترجمة المحسنة (من NavigationController)
String _createInstructionFromManeuverSmart(Map<String, dynamic> step) {
if (step['maneuver'] == null) return "تابع المسير";
if (step['maneuver'] == null) return "Continue straight".tr;
final maneuver = step['maneuver'];
final type = maneuver['type'] ?? 'continue';
@@ -1690,10 +1691,10 @@ class MapDriverController extends GetxController {
switch (type) {
case 'depart':
instruction = "انطلق";
instruction = "Go".tr;
break;
case 'arrive':
return "لقد وصلت إلى وجهتك، $name";
return "You have arrived at your destination, @name".trParams({'name': name});
case 'turn':
case 'fork':
case 'roundabout':
@@ -1705,14 +1706,14 @@ class MapDriverController extends GetxController {
_getTurnInstruction(modifier); // استخدم نفس دالتك المساعدة هنا
break;
default:
instruction = "تابع المسير";
instruction = "Continue straight".tr;
}
if (name.isNotEmpty) {
if (type == 'continue') {
instruction += " على $name";
instruction += " ${"on".tr} $name";
} else {
instruction += " نحو $name";
instruction += " ${"towards".tr} $name";
}
}
return instruction;
@@ -1728,10 +1729,10 @@ class MapDriverController extends GetxController {
switch (type) {
case 'depart':
instruction = "انطلق";
instruction = "Go".tr;
break;
case 'arrive':
instruction = "لقد وصلت إلى وجهتك";
instruction = "You have arrived at your destination".tr;
if (name.isNotEmpty) instruction += "، $name";
return instruction;
case 'turn':
@@ -1742,20 +1743,20 @@ class MapDriverController extends GetxController {
instruction = _getTurnInstruction(modifier);
break;
case 'continue':
instruction = "استمر";
instruction = "Continue".tr;
break;
default:
instruction = "اتجه";
instruction = "Head".tr;
}
if (name.isNotEmpty) {
if (instruction == "استمر") {
instruction += " على $name";
instruction += " ${"on".tr} $name";
} else {
instruction += " إلى $name";
instruction += " ${"to".tr} $name";
}
} else if (type == 'continue' && modifier == 'straight') {
instruction = "استمر بشكل مستقيم";
instruction = "Continue straight".tr;
}
return instruction;
@@ -1767,23 +1768,23 @@ class MapDriverController extends GetxController {
String _getTurnInstruction(String modifier) {
switch (modifier) {
case 'uturn':
return "قم بالاستدارة والعودة";
return "Make a U-turn".tr;
case 'sharp right':
return "انعطف يمينًا بحدة";
return "Turn sharp right".tr;
case 'right':
return "انعطف يمينًا";
return "Turn right".tr;
case 'slight right':
return "انعطف يمينًا قليلاً";
return "Turn slight right".tr;
case 'straight':
return "استمر بشكل مستقيم";
return "Continue straight".tr;
case 'slight left':
return "انعطف يسارًا قليلاً";
return "Turn slight left".tr;
case 'left':
return "انعطف يسارًا";
return "Turn left".tr;
case 'sharp left':
return "انعطف يسارًا بحدة";
return "Turn sharp left".tr;
default:
return "اتجه";
return "Head".tr;
}
}
@@ -1850,7 +1851,7 @@ class MapDriverController extends GetxController {
void _advanceStep() {
if (currentStepIndex >= _stepBounds.length - 1) {
// وصل للنهاية
currentInstruction = "لقد وصلت إلى وجهتك";
currentInstruction = "You have arrived at your destination".tr;
return;
}
@@ -1909,11 +1910,11 @@ class MapDriverController extends GetxController {
routeColor: Colors.blue);
} else {
if (Get.isDialogOpen == true) Get.back();
mySnackeBarError("يجب أن تكون أقرب من 100 متر للوصول");
mySnackeBarError("You must be closer than 100 meters to arrive".tr);
}
} catch (e) {
if (Get.isDialogOpen == true) Get.back();
mySnackeBarError("حدث خطأ في الاتصال");
mySnackeBarError("A connection error occurred".tr);
}
}
@@ -1941,9 +1942,7 @@ class MapDriverController extends GetxController {
// 2. فك تشفير البوليلاين الخاص بالخطوة وتحويله إلى LatLng
// -->> هنا تم التصحيح <<--
List<LatLng> pts = decodePolyline(s['polyline']['points'])
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
.toList();
List<LatLng> pts = PolylineUtils.decode(s['polyline']['points']);
// أضف نقاط البداية والنهاية إذا لم تكن موجودة في البوليلاين لضمان دقة الحدود
if (pts.isNotEmpty) {
@@ -1962,14 +1961,9 @@ class MapDriverController extends GetxController {
// A helper function to decode and convert the polyline string
List<LatLng> decodePolylineToLatLng(String polylineString) {
// 1. Decode the string into a list of number lists (e.g., [[lat, lng], ...])
List<List<num>> decodedPoints = decodePolyline(polylineString);
List<LatLng> decodedPoints = PolylineUtils.decode(polylineString);
// 2. Map each [lat, lng] pair to a LatLng object, ensuring conversion to double
List<LatLng> latLngPoints = decodedPoints
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
.toList();
return latLngPoints;
return decodedPoints;
}
// =================================================================
@@ -1986,11 +1980,11 @@ class MapDriverController extends GetxController {
void _suggestOptimization() {
Get.snackbar(
"تحسين أداء التطبيق",
"لضمان أفضل تجربة، نقترح تعديل الإعدادات لتناسب جهازك. هل تود المتابعة؟",
"Improve app performance".tr,
"To ensure the best experience, we suggest adjusting the settings to suit your device. Would you like to proceed?".tr,
duration: const Duration(seconds: 15),
mainButton: TextButton(
child: const Text("نعم، قم بالتحسين"),
child: Text("Yes, optimize".tr),
onPressed: () {
updateInterval.value = 8; // غير الفترة إلى 8 ثوانٍ
// save setting to shared_preferences
@@ -2017,7 +2011,8 @@ class MapDriverController extends GetxController {
Future<void> _fitToBounds(LatLngBounds b, {double padding = 60}) async {
// نستخدم الدالة الآمنة التي أنشأناها
await safeAnimateCamera(CameraUpdate.newLatLngBounds(b, padding));
await safeAnimateCamera(CameraUpdate.newLatLngBounds(b,
left: padding, top: padding, right: padding, bottom: padding));
}
double distanceBetweenDriverAndPassengerWhenConfirm = 0;
@@ -2154,7 +2149,8 @@ class MapDriverController extends GetxController {
LatLngBounds(northeast: northeast, southwest: southwest);
// Fit the camera to the bounds
var cameraUpdate = CameraUpdate.newLatLngBounds(boundsData, 140);
var cameraUpdate = CameraUpdate.newLatLngBounds(boundsData,
left: 140, top: 140, right: 140, bottom: 140);
safeAnimateCamera(cameraUpdate);
}
@@ -2395,22 +2391,19 @@ class MapDriverController extends GetxController {
polyLines.removeWhere((p) => p.polylineId.value == 'upcoming_route');
polyLines.removeWhere((p) => p.polylineId.value == 'traveled_route');
// المسار المتبقي (أزرق)
// المسار المتبقي
polyLines.add(Polyline(
polylineId: const PolylineId('upcoming_route'),
polylineId: const PolylineId("upcoming_route"),
points: remaining,
color: Colors.blue,
width: 8,
zIndex: 2,
startCap: Cap.roundCap,
endCap: Cap.roundCap,
color: isRideStarted ? Colors.blue : Colors.yellow,
));
// المسار المقطوع (رمادي)
polyLines.add(Polyline(
polylineId: const PolylineId('traveled_route'),
points: traveled,
color: Colors.grey.withOpacity(0.8),
color: Colors.grey.withValues(alpha: 0.8),
width: 7,
zIndex: 1,
));