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

@@ -8,7 +8,6 @@ import '../../../constant/links.dart';
import '../../../main.dart';
import '../../../views/home/Captin/history/history_details_page.dart';
import '../../functions/crud.dart';
import '../../functions/encrypt_decrypt.dart';
class HistoryCaptainController extends GetxController {
bool isloading = false;

View File

@@ -379,16 +379,20 @@ class LoginDriverController extends GetxController {
box.read(BoxName.tokenDriver).toString() ||
serverData['data'][0]['fingerPrint'].toString() !=
fingerPrint.toString()) {
await Get.defaultDialog(
Get.defaultDialog(
barrierDismissible: false,
title: 'Device Change Detected'.tr,
middleText: 'Please verify your identity'.tr,
textConfirm: 'Verify'.tr,
confirmTextColor: Colors.white,
onConfirm: () {
Get.back();
// انتقل لصفحة OTP الجديدة
Get.to(
// نغلق الـ Dialog أولاً بشكل صريح
if (Get.isDialogOpen ?? false) {
Get.back();
}
// ثم ننتقل لصفحة OTP
Get.offAll(
() => OtpVerificationPage(
phone: d['phone'].toString(),
deviceToken: fingerPrint.toString(),
@@ -399,6 +403,9 @@ class LoginDriverController extends GetxController {
);
},
);
isloading = false;
update();
return true; // نخرج من الدالة هنا لنسمح لـ OTP بالتعامل مع الأمر
}
// }
}

View File

@@ -1,15 +1,17 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ui';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay;
import 'package:get_storage/get_storage.dart';
import 'package:geolocator/geolocator.dart' as geo;
import '../../constant/box_name.dart';
import '../firebase/local_notification.dart';
const String notificationChannelId = 'driver_service_channel';
const int notificationId = 888;
@@ -35,14 +37,18 @@ Future<bool> onStart(ServiceInstance service) async {
IO.OptionBuilder()
.setTransports(['websocket'])
.disableAutoConnect()
.setQuery({'driver_id': driverId, 'token': token})
.setQuery({
'driver_id': driverId,
'token': token,
'EIO': '3', // توافقية مع Workerman
})
.setReconnectionAttempts(double.infinity)
.build());
socket.connect();
socket.onConnect((_) {
print("✅ Background Service: Socket Connected!");
print("✅ Background Service: Socket Connected! ID: ${socket?.id}");
if (service is AndroidServiceInstance) {
flutterLocalNotificationsPlugin.show(
id: notificationId,
@@ -70,39 +76,94 @@ Future<bool> onStart(ServiceInstance service) async {
final box = GetStorage();
bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false;
// 🔥 Check إضافي: هل الـ Overlay مفتوح بالفعل؟
bool overlayActive = await Overlay.FlutterOverlayWindow.isActive();
// 🔥 Check إضافي: هل الـ Overlay مفتوح بالفعل؟ (للأندرويد فقط)
bool overlayActive = false;
if (Platform.isAndroid) {
overlayActive = await Overlay.FlutterOverlayWindow.isActive();
}
if (isAppInForeground || overlayActive) {
print("🛑 App is FOREGROUND or Overlay already shown. Skipping.");
return;
}
// عرض الـ Overlay
print("🚀 App is BACKGROUND. Showing Overlay...");
try {
await Overlay.FlutterOverlayWindow.showOverlay(
enableDrag: true,
overlayTitle: "طلب جديد",
overlayContent: "لديك طلب جديد وصل للتو!",
flag: OverlayFlag.focusPointer,
positionGravity: PositionGravity.auto,
height: WindowSize.matchParent,
width: WindowSize.matchParent,
startPosition: const OverlayPosition(0, -30),
);
await Overlay.FlutterOverlayWindow.shareData(data);
} catch (e) {
print("Overlay Error: $e");
// عرض الـ Overlay (للأندرويد فقط)
if (Platform.isAndroid) {
print("🚀 App is BACKGROUND. Showing Overlay...");
try {
await Overlay.FlutterOverlayWindow.showOverlay(
enableDrag: true,
overlayTitle: "طلب جديد",
overlayContent: "لديك طلب جديد وصل للتو!",
flag: OverlayFlag.focusPointer,
positionGravity: PositionGravity.auto,
height: WindowSize.matchParent,
width: WindowSize.matchParent,
startPosition: const OverlayPosition(0, -30),
);
await Overlay.FlutterOverlayWindow.shareData(data);
} catch (e) {
print("Overlay Error: $e");
}
} else if (Platform.isIOS) {
// على iOS، نظهر إشعاراً عادياً لأن الـ Overlay غير موجود
flutterLocalNotificationsPlugin.show(
id: 1002,
title: "طلب رحلة جديد 🚖",
body: "لديك طلب رحلة جديد، افتح التطبيق للموافقة عليه",
notificationDetails: const NotificationDetails(
iOS: DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
),
),
payload: jsonEncode(data));
}
});
}
service.on('stopService').listen((event) {
socket?.disconnect();
socket?.clearListeners();
socket?.dispose();
service.stopSelf();
});
// 🔥 Location management in background isolate (Using Geolocator)
geo.Position? latestPos;
// Listen to location changes continuously in the background
geo.Geolocator.getPositionStream(
locationSettings: geo.AndroidSettings(
accuracy: geo.LocationAccuracy.high,
distanceFilter: 10,
intervalDuration: const Duration(seconds: 10),
),
).listen((pos) {
latestPos = pos;
});
// 🔥 MERCY HEARTBEAT: Send location every 2 minutes to keep driver active in 'raids'
Timer.periodic(const Duration(minutes: 2), (timer) async {
if (socket != null && socket.connected && latestPos != null) {
try {
socket.emit('update_location', {
'driver_id': driverId,
'lat': latestPos!.latitude,
'lng': latestPos!.longitude,
'heading': latestPos!.heading,
'speed': latestPos!.speed * 3.6,
'status': box.read(BoxName.statusDriverLocation) ?? 'on',
'source': 'background_heartbeat'
});
print(
"💓 Background Mercy Heartbeat Sent: ${latestPos!.latitude}, ${latestPos!.longitude}");
} catch (e) {
print("❌ Background Heartbeat Error: $e");
}
}
});
Timer.periodic(const Duration(seconds: 30), (timer) async {
if (service is AndroidServiceInstance) {
if (await service.isForegroundService()) {

View File

@@ -1,3 +1,4 @@
/*
import 'dart:convert';
import 'dart:io';
@@ -234,3 +235,4 @@ class CameraClassController extends GetxController {
super.onClose();
}
}
*/

View File

@@ -604,6 +604,59 @@ class CRUD {
);
return json.decode(response.body);
}
Future<dynamic> getMapSaas({
required String link,
}) async {
var url = Uri.parse(link);
try {
var response = await http.get(
url,
headers: {
'Content-Type': 'application/json',
'x-api-key': Env.mapSaasKey,
},
);
Log.print('link -MapSaas: $link');
Log.print('response -MapSaas: ${response.body}');
if (response.statusCode == 200) {
return jsonDecode(response.body);
}
Log.print('MapSaas Error: ${response.statusCode} - ${response.body}');
return null;
} catch (e) {
Log.print('MapSaas Exception: $e');
return null;
}
}
Future<dynamic> postMapSaas({
required String link,
required Map<String, dynamic> payload,
}) async {
var url = Uri.parse(link);
try {
var response = await http.post(
url,
body: jsonEncode(payload),
headers: {
'Content-Type': 'application/json',
'x-api-key': Env.mapSaasKey,
},
);
Log.print('post -MapSaas link: $link');
Log.print('post -MapSaas payload: $payload');
Log.print('post -MapSaas response: ${response.body}');
if (response.statusCode == 200 || response.statusCode == 201) {
return jsonDecode(response.body);
}
Log.print('MapSaas Post Error: ${response.statusCode} - ${response.body}');
return null;
} catch (e) {
Log.print('MapSaas Post Exception: $e');
return null;
}
}
}
class NoInternetException implements Exception {

View File

@@ -1,7 +1,11 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:get/get_core/src/get_main.dart';
import 'package:get/get_navigation/src/extension_navigation.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'package:sefer_driver/views/widgets/mydialoug.dart';
import 'background_service.dart';
@@ -28,18 +32,19 @@ class PermissionsHelper {
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
// Android 13+ (API 33+) يحتاج إذن POST_NOTIFICATIONS
if (androidInfo.version.sdkInt >= 33) {
final status = await Permission.notification.request();
if (status.isDenied) {
print('⚠️ إذن الإشعارات مرفوض');
mySnackbarWarning(
"يرجى منح صلاحية الإشعارات لضمان وصول الطلبات إليك");
return false;
}
if (status.isPermanentlyDenied) {
print('⚠️ إذن الإشعارات مرفوض بشكل دائم - افتح الإعدادات');
await openAppSettings();
mySnackbarWarning('يرجى فتح الإعدادات وتفعيل صلاحية الإشعارات');
return false;
}
}
@@ -50,25 +55,31 @@ class PermissionsHelper {
/// طلب جميع الإذونات المطلوبة
static Future<bool> requestAllPermissions() async {
// إذن الإشعارات أولاً
bool notificationGranted = await requestNotificationPermission();
if (!notificationGranted) return false;
// إذن الإشعارات (اختياري)
await requestNotificationPermission();
// إذن الموقع
final locationStatus = await Permission.location.request();
if (!locationStatus.isGranted) {
print('⚠️ إذن الموقع مرفوض');
// 1. طلب إذن الموقع الأساسي فقط إذا كان مرفوضاً
var status = await Permission.location.status;
if (status.isDenied) {
status = await Permission.location.request();
}
if (status.isPermanentlyDenied) {
_showSettingsDialog('الموقع');
return false;
}
// إذن الموقع في الخلفية
if (Platform.isAndroid) {
final bgLocationStatus = await Permission.locationAlways.request();
if (!bgLocationStatus.isGranted) {
print('⚠️ إذن الموقع في الخلفية مرفوض');
}
}
return status.isGranted || status.isLimited;
}
return true;
static void _showSettingsDialog(String permissionName) {
MyDialog().getDialog(
'صلاحية $permissionName مطلوبة',
'لقد قمت برفض صلاحية $permissionName سابقاً. يرجى تفعيلها من الإعدادات لتمكين التطبيق من العمل.',
() async {
await openAppSettings();
Get.back();
},
);
}
}

View File

@@ -1,15 +1,13 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:geolocator/geolocator.dart' as geo;
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:location/location.dart';
import 'package:battery_plus/battery_plus.dart';
import 'package:permission_handler/permission_handler.dart' as ph;
import 'package:sefer_driver/views/home/Captin/orderCaptin/order_request_page.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'package:sefer_driver/constant/table_names.dart';
import 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
@@ -17,6 +15,7 @@ import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../print.dart';
import '../firebase/local_notification.dart';
import '../home/captin/home_captain_controller.dart';
import '../home/captin/map_driver_controller.dart';
import '../home/payment/captain_wallet_controller.dart';
@@ -63,6 +62,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
bool _isPowerSavingMode = false;
final List<Map<String, dynamic>> _trackBuffer = [];
final List<Map<String, dynamic>> _behaviorBuffer = [];
LatLng? _lastPosForDistance;
LatLng? _lastRecordedRealLoc;
@@ -137,26 +137,22 @@ class LocationController extends GetxController with WidgetsBindingObserver {
Log.print("📱 Lifecycle: App is in FOREGROUND");
box.write(BoxName.isAppInForeground, true);
// إيقاف خدمة الخلفية لأننا في الواجهة الآن
// إيقاف خدمة الخلفية
BackgroundServiceHelper.stopService();
// التأكد من أن السوكيت متصل، وإذا لا، نعيد الاتصال فوراً
if (socket == null || !socket!.connected) {
Log.print("🔄 Socket disconnected in background. Reconnecting now...");
if (socket == null || (!socket!.connected && !_isInitializingSocket)) {
Log.print("🔄 Initializing Socket on resume...");
initSocket();
} else {
// إذا كان متصلاً، ننعش المستمعين فقط للتأكد
_setupSocketListeners();
}
} else if (state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) {
Log.print("📱 Lifecycle: App is in BACKGROUND");
box.write(BoxName.isAppInForeground, false);
// تشغيل خدمة الخلفية لضمان بقاء التطبيق حياً
BackgroundServiceHelper.startService();
// ملاحظة: لا نقطع السوكيت هنا، نتركه يعمل قدر الإمكان
// تشغيل خدمة الخلفية للأندرويد لضمان بقاء التطبيق حياً
if (!Platform.isIOS) {
BackgroundServiceHelper.startService();
}
}
}
@@ -179,69 +175,107 @@ class LocationController extends GetxController with WidgetsBindingObserver {
// ====== Socket Logic (Improved) ======
// ===================================================================
bool _isInitializingSocket = false;
void initSocket() {
// منع الاستدعاءات المتداخلة التي تسبب قتل الاتصال قبل اكتماله
if (_isInitializingSocket) {
Log.print("⏳ Socket is already initializing. Skipping redundant call.");
return;
}
if (socket != null && socket!.connected) {
Log.print("✅ Socket is already connected. No need to re-init.");
return;
}
String driverId = box.read(BoxName.driverID).toString();
String token = box.read(BoxName.tokenDriver).toString();
String platform = Platform.isIOS ? 'ios' : 'android';
// 1. إذا كان السوكيت موجوداً، فقط تأكد من اتصاله
_isInitializingSocket = true;
// تنظيف السوكيت القديم فقط إذا كان موجوداً وغير متصل
if (socket != null) {
if (!socket!.connected) {
Log.print("🟡 Socket exists but disconnected. Reconnecting...");
socket!.connect();
}
_setupSocketListeners(); // تحديث المستمعين
return;
Log.print("🧹 Cleaning up old socket instance...");
socket!.clearListeners();
socket!.dispose();
socket = null;
}
Log.print(
"🟡 [LocationController] Creating NEW Socket for Driver: $driverId");
"🟡 [LocationController] Initializing NEW Socket for Driver: $driverId");
// 2. إنشاء الاتصال
socket = IO.io(
'https://location.intaleq.xyz',
IO.OptionBuilder()
.setTransports(['websocket'])
.enableAutoConnect() // تفعيل إعادة الاتصال التلقائي
.setQuery(
{'driver_id': driverId, 'token': token, 'platform': platform})
.setReconnectionAttempts(double.infinity)
.setReconnectionDelay(2000)
.build());
try {
// العودة للـ Websocket حصراً لأنه الوحيد الذي ينجح في فتح القناة
socket = IO.io(
'https://location.intaleq.xyz',
IO.OptionBuilder()
.setTransports(['websocket'])
.setQuery({'driver_id': driverId, 'token': token, 'EIO': '3'})
.enableForceNew()
.build());
socket!.connect();
_setupSocketListeners();
_setupSocketListeners();
socket!.connect();
} catch (e) {
_isInitializingSocket = false;
Log.print("❌ Socket Initialization Exception: $e");
}
}
// دالة منفصلة لضمان عدم تكرار المستمعين
void _setupSocketListeners() {
if (socket == null) return;
// تنظيف القديم أولاً
socket!.off('connect');
socket!.off('disconnect');
socket!.off('new_ride_request');
socket!.off('ride_cancelled');
socket!.off('connect_error');
socket!.off('error');
socket!.onConnect((_) {
Log.print('✅ Socket Connected! ID: ${socket?.id}');
isSocketConnected = true;
_startHeartbeat();
_isInitializingSocket = false;
// ننتظر قليلاً للتأكد من تعبئة الـ IDs
Future.delayed(const Duration(milliseconds: 1000), () {
String? sid = socket?.id;
String? eid = socket?.io.engine?.id;
Log.print(
'✅ Socket Connected! ID: ${sid ?? eid ?? 'N/A'} (SID: $sid, EID: $eid)');
if (sid != null || eid != null) {
isSocketConnected = true;
_startHeartbeat();
}
});
});
socket!.onDisconnect((_) {
Log.print('❌ Socket Disconnected');
socket!.onDisconnect((data) {
_isInitializingSocket = false;
Log.print('❌ Socket Disconnected: $data');
isSocketConnected = false;
_stopHeartbeat();
});
socket!.onConnectError((err) {
_isInitializingSocket = false;
Log.print('❌ Socket Connect Error: $err');
});
socket!.onConnectTimeout((data) {
_isInitializingSocket = false;
Log.print('❌ Socket Connect Timeout: $data');
});
socket!.onError((err) {
_isInitializingSocket = false;
Log.print('❌ Socket General Error: $err');
});
socket!.on('reconnect_attempt', (attempt) {
Log.print('🔄 Socket Reconnecting... Attempt: $attempt');
});
// 🔥 الاستماع للطلبات الجديدة
socket!.on('new_ride_request', (data) {
Log.print("🔔 Socket: New Ride Request Arrived!");
@@ -296,8 +330,17 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (!isAppInForeground) {
Log.print(
"🛑 التطبيق في الخلفية. السوكيت سيتجاهل التوجيه ويترك المهمة للـ Overlay.");
return; // 👈 هذا السطر يمنع السوكيت من إكمال العمل وفتح الصفحة
"📱 [LocationController] Order received in background (iOS/Android). Source: $source");
if (Platform.isIOS) {
// على iOS، نقوم بإظهار إشعار محلي لأن الـ Overlay غير مدعوم
NotificationController().showNotification(
"طلب رحلة جديد 🚖",
"لديك طلب رحلة جديد، افتح التطبيق للموافقة عليه",
jsonEncode(rideData),
'ding.wav');
}
return;
}
try {
@@ -309,14 +352,23 @@ class LocationController extends GetxController with WidgetsBindingObserver {
// 3. تجهيز البيانات (DriverList)
List<dynamic> driverList = [];
if (rideData.length > 0) {
var sortedKeys = rideData.keys.map((e) => int.tryParse(e) ?? 0).toList()
..sort();
if (rideData.isNotEmpty) {
var sortedKeys = rideData.keys
.where((e) => int.tryParse(e) != null)
.map((e) => int.parse(e))
.toList()..sort();
for (var key in sortedKeys) {
driverList.add(rideData[key.toString()]);
}
}
// الحماية ضد البنية غير المكتملة
if (driverList.length <= 16) {
Log.print("❌ Socket Error: Parsed driver list is incomplete.");
return;
}
// 4. إغلاق النافذة (إن وجدت بالخطأ) والتنقل
try {
if (await TripOverlayPlugin.isOverlayActive()) {
@@ -459,21 +511,16 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_uploadBatchTimer?.cancel();
_socketHeartbeat?.cancel();
if (socket != null && socket!.connected) {
String driverId = box.read(BoxName.driverID).toString();
socket!.emit('update_location', {
'driver_id': driverId,
'lat': myLocation.latitude,
'lng': myLocation.longitude,
'heading': heading,
'speed': speed * 3.6,
'status': 'close', // Changed to off
'distance': totalDistance
});
socket!.disconnect();
if (socket != null) {
socket!.clearListeners();
socket!
.dispose(); // استخدام dispose بدلاً من disconnect لضمان تحرير الموارد على iOS
}
if (!Platform.isIOS) {
await BackgroundServiceHelper.stopService();
}
await BackgroundServiceHelper.stopService();
socket = null;
isSocketConnected = false;
_isReady = false;
@@ -526,16 +573,31 @@ class LocationController extends GetxController with WidgetsBindingObserver {
Future<void> _flushBufferToServer() async {
if (_trackBuffer.isEmpty) return;
List<Map<String, dynamic>> batch = List.from(_trackBuffer);
_trackBuffer.clear();
int itemsToTake = _trackBuffer.length > 100 ? 100 : _trackBuffer.length;
List<Map<String, dynamic>> batch = _trackBuffer.sublist(0, itemsToTake);
final String driverId = (box.read(BoxName.driverID) ?? '').toString();
try {
await CRUD().post(
var res = await CRUD().post(
link: '${AppLink.locationServer}/add_batch.php',
payload: {'driver_id': driverId, 'batch_data': jsonEncode(batch)},
);
if (res != 'failure') {
_trackBuffer.removeRange(0, itemsToTake);
} else {
_enforceBufferLimit();
}
} catch (e) {
Log.print('❌ Failed to upload batch: $e');
_enforceBufferLimit();
}
}
void _enforceBufferLimit() {
if (_trackBuffer.length > 500) {
_trackBuffer.removeRange(0, _trackBuffer.length - 500);
Log.print("⚠️ Buffer limit enforced. Removed oldest entries.");
}
}
@@ -547,32 +609,65 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (level >= powerSaveExitLevel) _isPowerSavingMode = false;
if (previousMode != _isPowerSavingMode) {
_startBatchTimers();
startLocationUpdates();
_updateLocationSettings();
}
});
}
Future<void> _updateLocationSettings() async {
if (_locSub == null) return;
int interval = _isPowerSavingMode ? 10000 : 5000;
try {
await location.changeSettings(
accuracy: LocationAccuracy.navigation,
interval: interval,
distanceFilter: _isPowerSavingMode ? 20 : 10,
);
Log.print("🔋 Location settings updated. Power Save: $_isPowerSavingMode");
} catch (e) {
Log.print("❌ Failed to update location settings: $e");
}
}
Future<void> _saveBehaviorIfMoved(LatLng pos, DateTime now,
{required double currentSpeed}) async {
final dist =
(_lastSqlLoc == null) ? 999.0 : _calculateDistance(_lastSqlLoc!, pos);
if (dist < 15.0) return;
final accel = _calcAcceleration(currentSpeed, now) ?? 0.0;
try {
await sql.insertData({
'driver_id': (box.read(BoxName.driverID) ?? '').toString(),
'latitude': pos.latitude,
'longitude': pos.longitude,
'acceleration': accel,
'created_at': now.toIso8601String(),
'updated_at': now.toIso8601String(),
}, TableName.behavior);
_lastSqlLoc = pos;
} catch (e) {
Log.print('SQLite Error: $e');
_lastSqlLoc = pos;
_behaviorBuffer.add({
'driver_id': (box.read(BoxName.driverID) ?? '').toString(),
'latitude': pos.latitude,
'longitude': pos.longitude,
'acceleration': accel,
'created_at': now.toIso8601String(),
'updated_at': now.toIso8601String(),
});
if (_behaviorBuffer.length >= 10) {
_flushBehaviorBuffer();
}
}
void _flushBehaviorBuffer() {
if (_behaviorBuffer.isEmpty) return;
List<Map<String, dynamic>> batch = List.from(_behaviorBuffer);
_behaviorBuffer.clear();
Future.microtask(() async {
try {
for (var data in batch) {
await sql.insertData(data, TableName.behavior);
}
} catch (e) {
Log.print('SQLite Batch Insert Error: $e');
}
});
}
// استبدال دالة Haversine اليدوية بـ Geolocator في باقي الكود أيضاً
// لأنها تعتمد على C++ في الأندرويد و Obj-C في الآيفون (Native Speed)
double _calculateDistance(LatLng a, LatLng b) {

View File

@@ -217,8 +217,12 @@ class SecurityHelper {
isNotTrust = await JailbreakRootDetection.instance.isNotTrust;
isJailBroken = await JailbreakRootDetection.instance.isJailBroken;
isRealDevice = await JailbreakRootDetection.instance.isRealDevice;
isOnExternalStorage =
await JailbreakRootDetection.instance.isOnExternalStorage;
// This method is only relevant/implemented for Android
if (Platform.isAndroid) {
isOnExternalStorage =
await JailbreakRootDetection.instance.isOnExternalStorage;
}
List<JailbreakIssue> issues =
await JailbreakRootDetection.instance.checkForIssues;
@@ -230,7 +234,6 @@ class SecurityHelper {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
bundleId = packageInfo.packageName;
if (bundleId.isNotEmpty) {
// Pass the CORRECT bundle ID to isTampered
isTampered = await JailbreakRootDetection.instance.isTampered(bundleId);
}
} catch (e) {

View File

@@ -3,15 +3,9 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'dart:ui' as ui; // للألوان
// import 'package:google_maps_flutter/google_maps_flutter.dart';
// import 'package:flutter_map/flutter_map.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:http/http.dart' as http;
// import 'package:latlong2/latlong.dart'
// as latlng; // هذا مهم جداً للتعامل مع إحداثيات OSM
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
import 'dart:async';
import '../../../constant/links.dart';
@@ -35,7 +29,8 @@ class HomeCaptainController extends GetxController {
Timer? activeTimer;
Map data = {};
bool isHomeMapActive = true;
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
InlqBitmap carIcon = InlqBitmap.defaultMarker;
bool isMapReadyForCommands = false;
bool isLoading = true;
late double kazan = 0;
double latePrice = 0;
@@ -55,7 +50,8 @@ class HomeCaptainController extends GetxController {
String totalMoneyInSEFER = '0';
String totalDurationToday = '0';
Timer? timer;
late LatLng myLocation = const LatLng(33.5138, 36.2765);
Timer? _cameraFollowTimer;
LatLng myLocation = const LatLng(33.5138, 36.2765);
String totalPoints = '0';
String countRefuse = '0';
bool mapType = false;
@@ -81,10 +77,13 @@ class HomeCaptainController extends GetxController {
// دالة لتغيير حالة الهيت ماب (عرض/إخفاء)
void toggleHeatmap() async {
isHeatmapVisible = !isHeatmapVisible;
print("🔥 [Heatmap] Visibility toggled to: $isHeatmapVisible");
if (isHeatmapVisible) {
await fetchAndDrawHeatmap();
startHeatmapCycle();
} else {
_heatmapTimer?.cancel();
heatmapPolygons.clear();
print("🧹 [Heatmap] Polygons cleared.");
}
update(); // تحديث الواجهة
}
@@ -96,6 +95,7 @@ class HomeCaptainController extends GetxController {
// دالة جلب البيانات ورسم الخريطة
Future<void> fetchAndDrawHeatmap() async {
print("🚀 [Heatmap] Fetching live data...");
// استخدم الرابط المباشر لملف JSON لسرعة قصوى
final String jsonUrl =
"https://api.intaleq.xyz/intaleq/ride/rides/heatmap_live.json";
@@ -107,20 +107,26 @@ class HomeCaptainController extends GetxController {
if (response.statusCode == 200) {
final List<dynamic> data = json.decode(response.body);
print("✅ [Heatmap] Data received. Points count: ${data.length}");
_generatePolygons(data);
} else {
print("⚠️ [Heatmap] Server error: ${response.statusCode}");
}
} catch (e) {
print("Heatmap Error: $e");
print("❌ [Heatmap] Error: $e");
}
}
void _generatePolygons(List<dynamic> data) {
print("🎨 [Heatmap] Processing polygons...");
Set<Polygon> tempPolygons = {};
// الأوفست لرسم المربع (نصف حجم الشبكة)
// الشبكة دقتها 0.01 درجة، لذا نصفها 0.005
double offset = 0.005;
int highCount = 0, medCount = 0, lowCount = 0;
for (var point in data) {
double lat = double.parse(point['lat'].toString());
double lng = double.parse(point['lng'].toString());
@@ -133,24 +139,27 @@ class HomeCaptainController extends GetxController {
// 🧠 منطق الألوان: ندمج الذكاء (Intensity) مع العدد (Count)
if (intensity == 'high' || count >= 5) {
highCount++;
// منطقة مشتعلة (أحمر)
// إما فيها طلبات ضائعة (Timeout) أو فيها عدد كبير من الطلبات
color = Colors.red.withOpacity(0.35);
strokeColor = Colors.red.withOpacity(0.8);
color = Colors.red.withValues(alpha: 0.35);
strokeColor = Colors.red.withValues(alpha: 0.8);
} else if (intensity == 'medium' || count >= 3) {
medCount++;
// منطقة متوسطة (برتقالي)
color = Colors.orange.withOpacity(0.35);
strokeColor = Colors.orange.withOpacity(0.8);
color = Colors.orange.withValues(alpha: 0.35);
strokeColor = Colors.orange.withValues(alpha: 0.8);
} else {
lowCount++;
// منطقة خفيفة (أصفر)
color = Colors.yellow.withOpacity(0.3);
strokeColor = Colors.yellow.withOpacity(0.6);
color = Colors.yellow.withValues(alpha: 0.3);
strokeColor = Colors.yellow.withValues(alpha: 0.6);
}
// رسم المربع
tempPolygons.add(Polygon(
polygonId: PolygonId("$lat-$lng"),
consumeTapEvents: true, // للسماح بالضغط عليه مستقبلاً
// consumeTapEvents: true, // للسماح بالضغط عليه مستقبلاً
points: [
LatLng(lat - offset, lng - offset),
LatLng(lat + offset, lng - offset),
@@ -164,13 +173,27 @@ class HomeCaptainController extends GetxController {
}
heatmapPolygons = tempPolygons;
print(
"✨ [Heatmap] Rendering Done. (🔥 High: $highCount, 🟠 Med: $medCount, 🟡 Low: $lowCount)");
print("📍 [Heatmap] Total Polygons on Map: ${heatmapPolygons.length}");
update(); // تحديث الخريطة
}
// دالة لتشغيل الخريطة الحرارية كل فترة (مثلاً عند فتح الصفحة)
Timer? _heatmapTimer;
// دالة لتشغيل الخريطة الحرارية كل فترة (كل 5 دقائق) لضمان نشاط البيانات
void startHeatmapCycle() {
_heatmapTimer?.cancel();
fetchAndDrawHeatmap();
// يمكن تفعيل Timer هنا لو أردت تحديثها تلقائياً كل 5 دقائق
_heatmapTimer = Timer.periodic(const Duration(minutes: 5), (timer) {
if (isHeatmapVisible) {
print("🔄 [Heatmap] Periodic refresh started...");
fetchAndDrawHeatmap();
} else {
timer.cancel();
}
});
}
void goToWalletFromConnect() {
@@ -185,16 +208,8 @@ class HomeCaptainController 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();
}
String stringActiveDuration = '';
@@ -259,7 +274,7 @@ class HomeCaptainController extends GetxController {
// 3. إظهار الديالوج المانع
Get.defaultDialog(
title: "حسابك مقيد مؤقتاً ⛔",
title: "Your account is temporarily restricted ⛔".tr,
titleStyle:
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
barrierDismissible: false, // 🚫 ممنوع الإغلاق بالضغط خارجاً
@@ -269,8 +284,9 @@ class HomeCaptainController extends GetxController {
const Icon(Icons.timer_off_outlined,
size: 50, color: Colors.orange),
const SizedBox(height: 15),
const Text(
"لقد تجاوزت حد الإلغاء المسموح به (3 مرات).\nلا يمكنك العمل حتى انتهاء العقوبة.",
Text(
"You have exceeded the allowed cancellation limit (3 times).\nYou cannot work until the penalty expires."
.tr,
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
@@ -296,11 +312,11 @@ class HomeCaptainController extends GetxController {
? () {
Get.back(); // إغلاق الديالوج
box.remove(BoxName.blockUntilDate); // إزالة الحظر
Get.snackbar("أهلاً بك", "يمكنك الآن استقبال الطلبات",
Get.snackbar("Welcome".tr, "You can now receive orders".tr,
backgroundColor: Colors.green);
}
: null, // زر معطل
child: Text(isFinished ? "Go Online" : "انتظر انتهاء الوقت"),
child: Text(isFinished ? "Go Online".tr : "Wait for timer".tr),
);
}),
);
@@ -334,7 +350,13 @@ class HomeCaptainController extends GetxController {
@override
void onClose() {
print("🔥 [HomeCaptain] onClose called. Tearing down map resources...");
_blockTimer?.cancel();
activeTimer?.cancel();
_cameraFollowTimer?.cancel();
_heatmapTimer?.cancel();
stopTimer();
mapHomeCaptainController = null;
super.onClose();
}
@@ -377,7 +399,8 @@ class HomeCaptainController extends GetxController {
confirm: MyElevatedButton(
title: 'Ok , See you Tomorrow'.tr,
onPressed: () {
Get.back();
// إغلاق الديالوج والعودة قسرياً
navigatorKey.currentState?.pop();
Get.back();
}));
}
@@ -395,26 +418,40 @@ class HomeCaptainController extends GetxController {
update();
}
// late GoogleMapController mapHomeCaptainController;
GoogleMapController? mapHomeCaptainController;
// final locationController = Get.find<LocationController>();
// late IntaleqMapController mapHomeCaptainController;
IntaleqMapController? mapHomeCaptainController;
// --- FIX 2: Smart Map Creation ---
void onMapCreated(GoogleMapController controller) {
void onMapCreated(IntaleqMapController controller) {
print("🔥 [HomeCaptain] onMapCreated started");
mapHomeCaptainController = controller;
// Check actual location before moving camera
var currentLoc = locationController.myLocation;
if (currentLoc.latitude != 0 && currentLoc.longitude != 0) {
controller.animateCamera(
CameraUpdate.newLatLng(currentLoc),
);
} else {
// Optional: Move to default city view instead of ocean
controller.animateCamera(
CameraUpdate.newLatLngZoom(myLocation, 10),
);
}
// We delay the first move to ensure the native side is fully ready
Future.delayed(const Duration(milliseconds: 800), () {
if (isClosed || mapHomeCaptainController == null) return;
try {
var currentLoc = locationController.myLocation;
if (currentLoc.latitude != 0 &&
currentLoc.latitude != null &&
!currentLoc.latitude.isNaN) {
print(
"🔥 [HomeCaptain] Safely moving camera to: ${currentLoc.latitude}");
mapHomeCaptainController!.moveCamera(
CameraUpdate.newLatLngZoom(currentLoc, 15),
);
} else {
print("🔥 [HomeCaptain] Safely moving to default Damascus");
mapHomeCaptainController!.moveCamera(
CameraUpdate.newLatLngZoom(myLocation, 12),
);
}
// Mark as ready for regular listener updates
isMapReadyForCommands = true;
} catch (e) {
print("❌ [HomeCaptain] Map move failed: $e");
}
});
}
void savePeriod(Duration period) {
@@ -441,7 +478,7 @@ class HomeCaptainController extends GetxController {
await getCaptainDurationOnToday();
String? initialDurationStr = totalDurationToday;
if (initialDurationStr != null) {
if (initialDurationStr != '0') {
// تحويل النص (01:20:30) إلى كائن Duration
List<String> parts = initialDurationStr.split(':');
_currentDuration = Duration(
@@ -490,16 +527,27 @@ class HomeCaptainController extends GetxController {
getlocation() async {
isLoading = true;
update();
// This ensures we try to get a fix, but map doesn't crash if it fails
await locationController.getLocation();
try {
// ننتظر جلب الموقع مع مهلة 10 ثوانٍ لتجنب التعليق
var locData = await locationController.getLocation().timeout(
const Duration(seconds: 10),
onTimeout: () => null,
);
var loc = locationController.myLocation;
if (loc.latitude != 0) {
myLocation = loc;
if (locData != null && locData.latitude != null) {
myLocation = LatLng(locData.latitude!, locData.longitude!);
print(
"📍 [HomeCaptain] Location updated: ${myLocation.latitude}, ${myLocation.longitude}");
} else {
print(
"⚠️ [HomeCaptain] Could not get current location, using default.");
}
} catch (e) {
print("❌ Error in getlocation: $e");
} finally {
isLoading = false;
update();
}
isLoading = false;
update();
}
Map walletDriverPointsDate = {};
@@ -535,27 +583,18 @@ class HomeCaptainController extends GetxController {
// 4. دالة نستدعيها عند العودة للصفحة الرئيسية
void resumeHomeMapUpdates() {
isHomeMapActive = true;
// إنعاش الخريطة عند العودة
if (mapHomeCaptainController != null) {
onMapCreated(mapHomeCaptainController!);
}
// تم حذف استدعاء onMapCreated المتكرر لمنع قفز الخريطة عند العودة
update();
}
@override
void onInit() async {
// ✅ طلب الإذونات أولاً
bool permissionsGranted = await PermissionsHelper.requestAllPermissions();
// ✅ تم إرجاعه كتعليق لمنع الديالوج عند التشغيل (كما كان في الكود الأصلي)
// bool permissionsGranted = await PermissionsHelper.requestAllPermissions();
// if (permissionsGranted) {
// await BackgroundServiceHelper.startService();
// }
if (permissionsGranted) {
// ✅ بدء الخدمة بعد الحصول على الإذونات
await BackgroundServiceHelper.startService();
print('✅ Background service started successfully');
} else {
print('❌ لم يتم منح الإذونات - الخدمة لن تعمل');
// اعرض رسالة للمستخدم
}
// await locationBackController.requestLocationPermission();
Get.put(FirebaseMessagesController());
addToken();
await getlocation();
@@ -575,29 +614,25 @@ class HomeCaptainController extends GetxController {
checkAndShowBlockDialog();
box.write(BoxName.statusDriverLocation, 'off');
// 2. عدل الليسنر ليصبح مشروطاً
locationController.addListener(() {
// الشرط الذهبي: إذا كانت الصفحة غير نشطة أو الخريطة غير موجودة، لا تفعل شيئاً
if (!isHomeMapActive || mapHomeCaptainController == null || isClosed)
return;
// 2. مؤقت التتبع التلقائي (كل 5 ثوانٍ كما في الكود السابق)
_cameraFollowTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
if (isClosed ||
!isHomeMapActive ||
mapHomeCaptainController == null ||
!isActive) return;
if (isActive) {
// isActive الخاصة بالزر "متصل/غير متصل"
var loc = locationController.myLocation;
if (loc.latitude != 0 && loc.longitude != 0) {
try {
mapHomeCaptainController!.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: loc,
zoom: 17.5,
tilt: 50.0,
bearing: locationController.heading,
),
),
var loc = locationController.myLocation;
if (loc.latitude != 0 && loc.latitude != null && !loc.latitude.isNaN) {
try {
// 🔥 Safety double-check before animating
if (mapHomeCaptainController != null) {
print("🔥 [HomeCaptain] Safely moving camera to: ${loc.latitude}");
mapHomeCaptainController?.animateCamera(
CameraUpdate.newLatLngZoom(loc, 17.5),
);
} catch (e) {
// التقاط الخطأ بصمت إذا حدث أثناء الانتقال
}
} catch (e) {
print("❌ [HomeCaptain] Camera movement failed: $e");
}
}
});
@@ -724,11 +759,4 @@ class HomeCaptainController extends GetxController {
update();
}
@override
void dispose() {
activeTimer?.cancel();
stopTimer();
mapHomeCaptainController?.dispose(); // Dispose controller
super.dispose();
}
}

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

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
class NavigationStep {
final String instruction;

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.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:sefer_driver/constant/api_key.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/constant/links.dart';
@@ -19,10 +18,10 @@ class NavigationService extends GetxService {
final RxSet<Polyline> polylines = <Polyline>{}.obs;
final RxString currentInstruction = "".obs;
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;
@override
void onInit() {
@@ -30,19 +29,11 @@ class NavigationService extends GetxService {
_loadCustomIcons();
}
void _loadCustomIcons() async {
carIcon = await _createBitmapDescriptor('assets/images/car.png');
passengerIcon = await _createBitmapDescriptor('assets/images/picker.png');
startIcon = await _createBitmapDescriptor('assets/images/A.png');
endIcon = await _createBitmapDescriptor('assets/images/b.png');
}
Future<BitmapDescriptor> _createBitmapDescriptor(String assetName) {
return BitmapDescriptor.fromAssetImage(
ImageConfiguration(
size: const Size(30, 35), devicePixelRatio: Get.pixelRatio),
assetName,
);
void _loadCustomIcons() {
carIcon = InlqBitmap.fromAsset('assets/images/car.png');
passengerIcon = InlqBitmap.fromAsset('assets/images/picker.png');
startIcon = InlqBitmap.fromAsset('assets/images/A.png');
endIcon = InlqBitmap.fromAsset('assets/images/b.png');
}
Future<Map<String, dynamic>?> getRoute({
@@ -62,9 +53,7 @@ class NavigationService extends GetxService {
void drawRoute(Map<String, dynamic> routeData, {Color color = Colors.blue}) {
final pointsString = routeData["overview_polyline"]["points"];
final points = decodePolyline(pointsString)
.map((p) => LatLng(p[0].toDouble(), p[1].toDouble()))
.toList();
final points = PolylineUtils.decode(pointsString);
final polyline = Polyline(
polylineId: PolylineId(routeData["summary"] ?? DateTime.now().toString()),

View File

@@ -5,14 +5,16 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'package:just_audio/just_audio.dart';
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
import 'dart:math' as math;
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../env/env.dart';
import '../../../main.dart';
import '../../../print.dart';
import '../../../views/home/Captin/driver_map_page.dart';
@@ -22,8 +24,6 @@ import '../../firebase/local_notification.dart';
import '../../functions/crud.dart';
import '../../functions/location_controller.dart';
import '../../home/captin/home_captain_controller.dart';
import '../../firebase/notification_service.dart';
import '../navigation/decode_polyline_isolate.dart';
class OrderRequestController extends GetxController
with WidgetsBindingObserver {
@@ -40,7 +40,7 @@ class OrderRequestController extends GetxController
bool _isRideTakenHandled = false;
// --- الأيقونات والماركرز ---
BitmapDescriptor? driverIcon;
InlqBitmap? driverIcon;
Map<MarkerId, Marker> markersMap = {};
Set<Marker> get markers => markersMap.values.toSet();
@@ -49,7 +49,7 @@ class OrderRequestController extends GetxController
List<dynamic>? myList;
Map<dynamic, dynamic>? myMapData;
GoogleMapController? mapController;
IntaleqMapController? mapController;
// الإحداثيات (أزلنا late لتجنب الأخطاء القاتلة)
double latPassenger = 0.0;
@@ -64,7 +64,7 @@ class OrderRequestController extends GetxController
String totalTripDuration = "--";
String tripPrice = "--";
String timeToPassenger = "جاري الحساب...";
String timeToPassenger = "Calculating...".tr;
String distanceToPassenger = "--";
// --- الخريطة ---
@@ -193,9 +193,25 @@ class OrderRequestController extends GetxController
void _parseExtraData() {
passengerRating = _safeGet(33).isEmpty ? "5.0" : _safeGet(33);
tripType = _safeGet(31);
totalTripDistance = _safeGet(5);
totalTripDuration = _safeGet(19);
tripPrice = _safeGet(2);
// Format numbers to avoid many decimal places
String rawDist = _safeGet(5);
if (rawDist.isNotEmpty) {
double? d = double.tryParse(rawDist);
totalTripDistance = d != null ? "${d.toStringAsFixed(1)} km" : rawDist;
}
String rawDur = _safeGet(19);
if (rawDur.isNotEmpty) {
double? d = double.tryParse(rawDur);
totalTripDuration = d != null ? "${d.toStringAsFixed(0)} min" : rawDur;
}
String rawPrice = _safeGet(2);
if (rawPrice.isNotEmpty) {
double? p = double.tryParse(rawPrice);
tripPrice = p != null ? p.toStringAsFixed(0) : rawPrice;
}
}
// ----------------------------------------------------------------------
@@ -203,6 +219,8 @@ class OrderRequestController extends GetxController
// ----------------------------------------------------------------------
Future<void> _calculateFullJourney() async {
if (mapController == null) return; // Wait for controller to draw
try {
Position driverPos = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
@@ -210,6 +228,9 @@ class OrderRequestController extends GetxController
updateDriverLocation(driverLatLng, driverPos.heading);
// Clear old polylines to avoid "ghost lines"
polylines.clear();
var pickupFuture = _fetchRouteData(
start: driverLatLng,
end: LatLng(latPassenger, lngPassenger),
@@ -246,7 +267,8 @@ class OrderRequestController extends GetxController
destTime: totalTripDuration,
destDist: totalTripDistance);
zoomToFitRide(driverLatLng);
// Now zoom to fit all polylines and markers
zoomToFitRide();
update();
} catch (e) {
@@ -254,6 +276,22 @@ class OrderRequestController extends GetxController
}
}
String _formatDistance(dynamic rawDist) {
if (rawDist == null || rawDist.toString().isEmpty) return "--";
double dist = double.tryParse(rawDist.toString()) ?? 0.0;
if (dist <= 0) return "--";
if (dist < 1000) return "${dist.toStringAsFixed(0)} m";
return "${(dist / 1000).toStringAsFixed(1)} km";
}
String _formatDuration(dynamic rawDur) {
if (rawDur == null || rawDur.toString().isEmpty) return "--";
double dur = double.tryParse(rawDur.toString()) ?? 0.0;
if (dur <= 0) return "1 min"; // Minimum 1 min for UI
if (dur < 60) return "${dur.toStringAsFixed(0)} sec";
return "${(dur / 60).toStringAsFixed(0)} min";
}
Future<Map<String, dynamic>?> _fetchRouteData(
{required LatLng start,
required LatLng end,
@@ -261,46 +299,56 @@ class OrderRequestController extends GetxController
required String id,
bool isDashed = false}) async {
try {
// حماية من الإحداثيات الصفرية
if (start.latitude == 0 || end.latitude == 0) return null;
if (mapController == null) return null;
String apiUrl = "https://routesy.intaleq.xyz/route/v1/driving";
String coords =
"${start.longitude},${start.latitude};${end.longitude},${end.latitude}";
String url = "$apiUrl/$coords?steps=false&overview=full";
final saasUrl = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: {
'fromLat': start.latitude.toString(),
'fromLng': start.longitude.toString(),
'toLat': end.latitude.toString(),
'toLng': end.longitude.toString(),
'steps': 'false',
'alternatives': 'false',
});
var response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
var json = jsonDecode(response.body);
if (json['code'] == 'Ok' && json['routes'].isNotEmpty) {
var route = json['routes'][0];
final response = await http.get(saasUrl, headers: {
'x-api-key': Env.mapSaasKey,
'Content-Type': 'application/json',
});
double distM = double.parse(route['distance'].toString());
double durS = double.parse(route['duration'].toString());
if (response.statusCode != 200) {
throw Exception("Routing request failed: ${response.statusCode}");
}
String distText = "${(distM / 1000).toStringAsFixed(1)} كم";
String durText = "${(durS / 60).toStringAsFixed(0)} دقيقة";
final data = jsonDecode(response.body);
print("🛣️ Route API Response [$id]: ${data}");
String geometry = route['geometry'];
List<LatLng> points = await compute(decodePolylineIsolate, geometry);
// The map-saas API returns the route data directly at the root,
// with 'points' being an encoded polyline string.
final String? encodedPoints = data['points']?.toString();
Polyline polyline = Polyline(
polylineId: PolylineId(id),
color: color,
width: 5,
points: points,
patterns:
isDashed ? [PatternItem.dash(10), PatternItem.gap(5)] : [],
startCap: Cap.roundCap,
endCap: Cap.roundCap,
);
if (encodedPoints != null && encodedPoints.isNotEmpty) {
List<LatLng> path = controllerDecodePolyline(encodedPoints);
print("📍 Path for [$id] has ${path.length} points.");
return {
'distance_text': distText,
'duration_text': durText,
'polyline': polyline
};
}
final num? rawDist = data['distance'] is num ? data['distance'] : null;
final num? rawDur = data['duration'] is num ? data['duration'] : null;
final distanceText = data['distance_text'] ?? _formatDistance(rawDist);
final durationText = data['duration_text'] ?? _formatDuration(rawDur);
Polyline polyline = Polyline(
polylineId: PolylineId(id),
color: color,
width: 5,
points: path,
);
return {
'distance_text': distanceText,
'duration_text': durationText,
'polyline': polyline
};
}
} catch (e) {
print("Route Fetch Error: $e");
@@ -308,36 +356,47 @@ class OrderRequestController extends GetxController
return null;
}
void zoomToFitRide(LatLng driverPos) {
void zoomToFitRide() {
if (mapController == null) return;
// حماية من النقاط الصفرية
if (latPassenger == 0 || latDestination == 0) return;
List<LatLng> allPoints = [];
List<LatLng> points = [
driverPos,
LatLng(latPassenger, lngPassenger),
LatLng(latDestination, lngDestination),
];
// Add all polyline points to the bounds calculation
for (var polyline in polylines) {
allPoints.addAll(polyline.points);
}
double minLat = points.first.latitude;
double maxLat = points.first.latitude;
double minLng = points.first.longitude;
double maxLng = points.first.longitude;
// Fallback to basic markers if polylines are empty
if (allPoints.isEmpty) {
allPoints.addAll([
LatLng(latPassenger, lngPassenger),
LatLng(latDestination, lngDestination),
]);
}
for (var p in points) {
if (allPoints.isEmpty) return;
double minLat = allPoints.first.latitude;
double maxLat = allPoints.first.latitude;
double minLng = allPoints.first.longitude;
double maxLng = allPoints.first.longitude;
for (var p in allPoints) {
if (p.latitude < minLat) minLat = p.latitude;
if (p.latitude > maxLat) maxLat = p.latitude;
if (p.longitude < minLng) minLng = p.longitude;
if (p.longitude > maxLng) maxLng = p.longitude;
}
// Add some padding to the bounds
double latPad = (maxLat - minLat) * 0.25;
double lngPad = (maxLng - minLng) * 0.2;
mapController!.animateCamera(CameraUpdate.newLatLngBounds(
LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
southwest: LatLng(minLat - latPad, minLng - lngPad),
northeast: LatLng(maxLat + latPad, maxLng + lngPad),
),
100.0,
));
}
@@ -357,15 +416,15 @@ class OrderRequestController extends GetxController
// حماية إذا لم يتم جلب الإحداثيات
if (latPassenger == 0 || latDestination == 0) return;
final BitmapDescriptor pickupIcon =
final InlqBitmap pickupIcon =
await MarkerGenerator.createCustomMarkerBitmap(
title: paxTime,
subtitle: paxDist,
color: Colors.green.shade700,
color: Colors.amber.shade900, // Matching the amber pickup line
iconData: Icons.person_pin_circle,
);
final BitmapDescriptor dropoffIcon =
final InlqBitmap dropoffIcon =
await MarkerGenerator.createCustomMarkerBitmap(
title: destTime ?? totalTripDuration,
subtitle: destDist ?? totalTripDistance,
@@ -411,10 +470,9 @@ class OrderRequestController extends GetxController
points: [driverLatLng, LatLng(latPassenger, lngPassenger)],
color: Colors.grey,
width: 2,
patterns: [PatternItem.dash(10), PatternItem.gap(10)],
));
zoomToFitRide(driverLatLng);
zoomToFitRide();
}
update();
@@ -435,8 +493,9 @@ class OrderRequestController extends GetxController
}
}
void onMapCreated(GoogleMapController controller) {
void onMapCreated(IntaleqMapController controller) {
mapController = controller;
_calculateFullJourney();
}
// --- قبول الطلب وإدارة التايمر ---
@@ -488,10 +547,13 @@ class OrderRequestController extends GetxController
// 1. حذف الإشعار من شريط التنبيهات فوراً
NotificationController().cancelOrderNotification();
if (Get.isSnackbarOpen) Get.closeCurrentSnackbar();
if (Get.isDialogOpen ?? false) Get.back();
// إغلاق أي ديالوج مفتوح قسرياً
if (Get.isDialogOpen ?? false) {
navigatorKey.currentState?.pop();
}
Get.back();
Get.snackbar("تنبيه", "تم قبول الطلب من قبل سائق آخر",
backgroundColor: Colors.orange, colorText: Colors.white);
mySnackbarInfo("The order has been accepted by another driver.".tr);
}
});
}
@@ -546,9 +608,10 @@ class OrderRequestController extends GetxController
if (isFailure) {
// ⛔ حالة الفشل: الطلب مأخوذ
MyDialog().getDialog("عذراً، الطلب أخذه سائق آخر.", '', () {
Get.back(); // إغلاق الديالوج
Get.back(); // العودة للصفحة الرئيسية (إغلاق صفحة الطلب)
MyDialog().getDialog(
"Sorry, the order was taken by another driver.".tr, '', () {
// بما أن MyDialog يغلق نفسه الآن، نحتاج Get.back() واحدة فقط لإغلاق صفحة الطلب
Get.back();
});
} else {
// ✅ حالة النجاح
@@ -611,7 +674,37 @@ class OrderRequestController extends GetxController
audioPlayer.dispose();
WidgetsBinding.instance.removeObserver(this);
_timer?.cancel();
mapController?.dispose();
// mapController?.dispose();
super.onClose();
}
List<LatLng> controllerDecodePolyline(String encoded) {
List<LatLng> points = [];
int index = 0, len = encoded.length;
int lat = 0, lng = 0;
while (index < len) {
int b, shift = 0, result = 0;
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lat += dlat;
shift = 0;
result = 0;
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lng += dlng;
points.add(LatLng(lat / 1E5, lng / 1E5));
}
return points;
}
}

View File

@@ -1,16 +1,6 @@
import 'package:google_maps_flutter/google_maps_flutter.dart';
// تم تعديل الدالة لتقبل وسيط من نوع `dynamic` لحل مشكلة عدم تطابق الأنواع مع دالة `compute`.
// هذه الدالة لا تزال تعمل كدالة من المستوى الأعلى (Top-level function)
// وهو شرط أساسي لاستخدامها مع دالة compute.
List<LatLng> decodePolylineIsolate(dynamic encodedMessage) {
// التأكد من أن الرسالة المستقبلة هي من نوع String
if (encodedMessage is! String) {
// إرجاع قائمة فارغة أو إظهار خطأ إذا كان النوع غير صحيح
return [];
}
final String encoded = encodedMessage;
import 'package:intaleq_maps/intaleq_maps.dart';
List<LatLng> decodePolylineIsolate(String encoded) {
List<LatLng> points = [];
int index = 0, len = encoded.length;
int lat = 0, lng = 0;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
import 'dart:async';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'dart:math';
/// Worker entrypoint (spawnUri/spawn).

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:sefer_driver/constant/style.dart';
import '../../constant/api_key.dart';

View File

@@ -4,109 +4,57 @@ import 'package:get/get.dart';
import '../../constant/box_name.dart';
import '../../main.dart';
import '../themes/themes.dart';
import '../profile/setting_controller.dart';
class LocaleController extends GetxController {
Locale? language;
String countryCode = '';
ThemeData appTheme = lightThemeEnglish;
ThemeData get appTheme {
String lang = box.read(BoxName.lang) ?? 'en';
bool isDarkMode = box.read('isDarkMode') ?? false;
return _getThemeFor(lang, isDarkMode);
}
ThemeData get lightTheme {
String lang = box.read(BoxName.lang) ?? 'en';
return _getThemeFor(lang, false);
}
ThemeData get darkTheme {
String lang = box.read(BoxName.lang) ?? 'en';
return _getThemeFor(lang, true);
}
ThemeData _getThemeFor(String lang, bool isDarkMode) {
if (lang.startsWith('ar')) {
return isDarkMode ? darkThemeArabic : lightThemeArabic;
} else {
return isDarkMode ? darkThemeEnglish : lightThemeEnglish;
}
}
void refreshTheme() {
update();
}
void changeLang(String langcode) {
Locale locale;
switch (langcode) {
case "ar":
locale = const Locale("ar");
appTheme = lightThemeArabic;
box.write(BoxName.lang, 'ar');
break;
case "en":
locale = const Locale("en");
appTheme = lightThemeEnglish;
box.write(BoxName.lang, 'en');
break;
case "tr":
locale = const Locale("tr");
appTheme = lightThemeEnglish;
box.write(BoxName.lang, 'tr');
break;
case "fr":
locale = const Locale("fr");
appTheme = lightThemeEnglish;
box.write(BoxName.lang, 'fr');
break;
case "it":
locale = const Locale("it");
appTheme = lightThemeEnglish;
box.write(BoxName.lang, 'it');
break;
case "de":
locale = const Locale("de");
appTheme = lightThemeEnglish;
box.write(BoxName.lang, 'de');
break;
case "el":
locale = const Locale("el");
appTheme = lightThemeEnglish;
box.write(BoxName.lang, 'el');
break;
case "es":
locale = const Locale("es");
appTheme = lightThemeEnglish;
box.write(BoxName.lang, 'es');
break;
case "fa":
locale = const Locale("fa");
appTheme = lightThemeEnglish;
box.write(BoxName.lang, 'fa');
break;
case "zh":
locale = const Locale("zh");
appTheme = lightThemeEnglish;
box.write(BoxName.lang, 'zh');
break;
case "ru":
locale = const Locale("ru");
appTheme = lightThemeEnglish;
box.write(BoxName.lang, 'ru');
break;
case "hi":
locale = const Locale("hi");
appTheme = lightThemeEnglish;
box.write(BoxName.lang, 'hi');
break;
case "ar-ma":
locale = const Locale("ar-ma");
appTheme = lightThemeArabic;
box.write(BoxName.lang, 'ar-ma');
break;
case "ar-gulf":
locale = const Locale("ar-gulf");
appTheme = lightThemeArabic;
box.write(BoxName.lang, 'ar-gulf');
break;
default:
locale = Locale(Get.deviceLocale!.languageCode);
box.write(BoxName.lang, Get.deviceLocale!.languageCode);
appTheme = lightThemeEnglish;
break;
}
Locale locale = Locale(langcode);
box.write(BoxName.lang, langcode);
Get.changeTheme(appTheme);
Get.updateLocale(locale);
update();
}
@override
void onInit() {
String? storedLang = box.read(BoxName.lang);
if (storedLang == null) {
// Use device language if no language is stored
storedLang = Get.deviceLocale!.languageCode;
box.write(BoxName.lang, storedLang);
}
changeLang(storedLang);
language = Locale(storedLang);
super.onInit();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -18,9 +18,7 @@ import '../../constant/colors.dart';
import '../../constant/info.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../print.dart';
import '../functions/crud.dart';
import '../functions/encrypt_decrypt.dart';
import '../functions/toast.dart';
import 'paymob/paymob_wallet.dart';
@@ -170,8 +168,8 @@ class PaymentController extends GetxController {
)),
allowsDelayedPaymentMethods: true,
customerEphemeralKeySecret: Stripe.merchantIdentifier,
appearance: const PaymentSheetAppearance(
shapes: PaymentSheetShape(borderRadius: 12),
appearance: PaymentSheetAppearance(
shapes: const PaymentSheetShape(borderRadius: 12),
colors: PaymentSheetAppearanceColors(
background: AppColor.secondaryColor,
),
@@ -725,7 +723,6 @@ class PaymentController extends GetxController {
box.write(BoxName.passengerWalletTotal, '0');
}
// getPassengerWallet();
final localAuth = LocalAuthentication();
super.onInit();
}
}

View File

@@ -1,28 +1,45 @@
import 'package:flutter/material.dart';
import 'package:sefer_driver/constant/box_name.dart';
import 'package:sefer_driver/main.dart';
import 'package:get/get.dart';
import '../local/local_controller.dart';
class SettingController extends GetxController {
bool isGoogleMapsEnabled = false;
bool isMapDarkMode = false;
bool isDarkMode = false;
void onChangMapApp() {
if (!isGoogleMapsEnabled) {
isGoogleMapsEnabled = true;
box.write(BoxName.googlaMapApp, true);
update();
} else {
isGoogleMapsEnabled = false;
box.write(BoxName.googlaMapApp, false);
update();
isGoogleMapsEnabled = !isGoogleMapsEnabled;
box.write(BoxName.googlaMapApp, isGoogleMapsEnabled);
update();
}
void toggleMapTheme() {
isMapDarkMode = !isMapDarkMode;
box.write('isMapDarkMode', isMapDarkMode);
update();
}
void toggleAppTheme() {
isDarkMode = !isDarkMode;
box.write('isDarkMode', isDarkMode);
// Update the theme using the LocaleController to ensure correct fonts are used
if (Get.isRegistered<LocaleController>()) {
Get.find<LocaleController>().refreshTheme();
}
update();
}
@override
void onInit() {
if (box.read(BoxName.googlaMapApp) != null) {
isGoogleMapsEnabled = box.read(BoxName.googlaMapApp);
}
update();
isGoogleMapsEnabled = box.read(BoxName.googlaMapApp) ?? false;
isMapDarkMode = box.read('isMapDarkMode') ?? false;
isDarkMode = box.read('isDarkMode') ?? false;
super.onInit();
}
}

View File

@@ -2,146 +2,89 @@ import 'package:flutter/material.dart';
import 'package:sefer_driver/constant/style.dart';
import '../../constant/colors.dart';
ThemeData lightThemeEnglish = ThemeData(
ThemeData _createTheme({
required Brightness brightness,
required String fontFamily,
required Color scaffoldBackgroundColor,
required Color cardColor,
required Color dividerColor,
}) {
return ThemeData(
brightness: brightness,
fontFamily: fontFamily,
scaffoldBackgroundColor: scaffoldBackgroundColor,
cardColor: cardColor,
dividerColor: dividerColor,
primaryColor: AppColor.primaryColor,
colorScheme: ColorScheme.fromSeed(
seedColor: AppColor.primaryColor,
brightness: brightness,
primary: AppColor.primaryColor,
surface: cardColor,
background: scaffoldBackgroundColor,
error: AppColor.redColor,
),
textTheme: TextTheme(
displaySmall: AppStyle.title.copyWith(color: AppColor.writeColor),
displayLarge: AppStyle.headTitle.copyWith(color: AppColor.writeColor),
displayMedium: AppStyle.headTitle2.copyWith(color: AppColor.writeColor),
bodyLarge: AppStyle.title.copyWith(color: AppColor.writeColor),
bodyMedium: AppStyle.subtitle.copyWith(color: AppColor.writeColor),
titleLarge: AppStyle.headTitle2.copyWith(color: AppColor.writeColor),
),
appBarTheme: AppBarTheme(
elevation: 0,
backgroundColor: scaffoldBackgroundColor,
centerTitle: true,
iconTheme: const IconThemeData(color: AppColor.primaryColor),
titleTextStyle: AppStyle.headTitle2.copyWith(color: AppColor.writeColor),
),
dialogTheme: DialogThemeData(
backgroundColor: cardColor,
contentTextStyle: AppStyle.title.copyWith(color: AppColor.writeColor),
titleTextStyle: AppStyle.headTitle2.copyWith(color: AppColor.writeColor),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
cardTheme: CardThemeData(
color: cardColor,
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
dividerTheme: DividerThemeData(
color: dividerColor,
thickness: 1,
),
);
}
ThemeData lightThemeEnglish = _createTheme(
brightness: Brightness.light,
fontFamily: "SFPro",
textTheme: TextTheme(
displaySmall: AppStyle.title,
displayLarge: AppStyle.headTitle,
displayMedium: AppStyle.headTitle2,
bodyLarge: AppStyle.title,
bodyMedium: AppStyle.subtitle,
),
primarySwatch: Colors.blue,
dialogTheme: DialogThemeData(
backgroundColor: AppColor.secondaryColor,
contentTextStyle: AppStyle.title,
titleTextStyle: AppStyle.headTitle2,
),
appBarTheme: AppBarTheme(
elevation: 0,
color: AppColor.secondaryColor,
centerTitle: true,
iconTheme: const IconThemeData(
color: AppColor.primaryColor,
),
toolbarTextStyle: TextTheme(
titleSmall: AppStyle.subtitle,
headlineSmall: AppStyle.title,
titleLarge: AppStyle.headTitle2,
).bodyMedium,
titleTextStyle: TextTheme(
titleSmall: AppStyle.subtitle,
headlineSmall: AppStyle.title,
titleLarge: AppStyle.headTitle2,
).titleLarge,
),
scaffoldBackgroundColor: Colors.white,
cardColor: Colors.white,
dividerColor: Colors.black12,
);
ThemeData darkThemeEnglish = ThemeData(
ThemeData darkThemeEnglish = _createTheme(
brightness: Brightness.dark,
fontFamily: "SFPro",
textTheme: TextTheme(
displaySmall: AppStyle.title,
displayLarge: AppStyle.headTitle,
displayMedium: AppStyle.headTitle2,
bodyLarge: AppStyle.title,
bodyMedium: AppStyle.subtitle,
),
primarySwatch: Colors.blue,
dialogTheme: DialogThemeData(
backgroundColor: AppColor.secondaryColor,
contentTextStyle: AppStyle.title,
titleTextStyle: AppStyle.headTitle2,
),
appBarTheme: AppBarTheme(
elevation: 0,
color: AppColor.secondaryColor,
centerTitle: true,
iconTheme: const IconThemeData(
color: AppColor.primaryColor,
),
toolbarTextStyle: TextTheme(
titleSmall: AppStyle.subtitle,
headlineSmall: AppStyle.title,
titleLarge: AppStyle.headTitle2,
).bodyMedium,
titleTextStyle: TextTheme(
titleSmall: AppStyle.subtitle,
headlineSmall: AppStyle.title,
titleLarge: AppStyle.headTitle2,
).titleLarge,
),
scaffoldBackgroundColor: const Color(0xFF121212),
cardColor: const Color(0xFF1E1E1E),
dividerColor: Colors.white10,
);
ThemeData lightThemeArabic = ThemeData(
ThemeData lightThemeArabic = _createTheme(
brightness: Brightness.light,
fontFamily: 'SFArabic',
textTheme: TextTheme(
displaySmall: AppStyle.title,
displayLarge: AppStyle.headTitle,
displayMedium: AppStyle.headTitle2,
bodyLarge: AppStyle.title,
bodyMedium: AppStyle.subtitle,
),
primarySwatch: Colors.blue,
dialogTheme: DialogThemeData(
backgroundColor: AppColor.secondaryColor,
contentTextStyle: AppStyle.title,
titleTextStyle: AppStyle.headTitle2,
),
appBarTheme: AppBarTheme(
elevation: 0,
color: AppColor.secondaryColor,
centerTitle: true,
iconTheme: const IconThemeData(
color: AppColor.primaryColor,
),
toolbarTextStyle: TextTheme(
titleSmall: AppStyle.subtitle,
headlineSmall: AppStyle.title,
titleLarge: AppStyle.headTitle2,
).bodyMedium,
titleTextStyle: TextTheme(
titleSmall: AppStyle.subtitle,
headlineSmall: AppStyle.title,
titleLarge: AppStyle.headTitle2,
).titleLarge,
),
scaffoldBackgroundColor: Colors.white,
cardColor: Colors.white,
dividerColor: Colors.black12,
);
ThemeData darkThemeArabic = ThemeData(
ThemeData darkThemeArabic = _createTheme(
brightness: Brightness.dark,
fontFamily: 'SFArabic',
textTheme: TextTheme(
displaySmall: AppStyle.title,
displayLarge: AppStyle.headTitle,
displayMedium: AppStyle.headTitle2,
bodyLarge: AppStyle.title,
bodyMedium: AppStyle.subtitle,
),
primarySwatch: Colors.blue,
dialogTheme: DialogThemeData(
backgroundColor: AppColor.secondaryColor,
contentTextStyle: AppStyle.title,
titleTextStyle: AppStyle.headTitle2,
),
appBarTheme: AppBarTheme(
elevation: 0,
color: AppColor.secondaryColor,
centerTitle: true,
iconTheme: const IconThemeData(
color: AppColor.primaryColor,
),
toolbarTextStyle: TextTheme(
titleSmall: AppStyle.subtitle,
headlineSmall: AppStyle.title,
titleLarge: AppStyle.headTitle2,
).bodyMedium,
titleTextStyle: TextTheme(
titleSmall: AppStyle.subtitle,
headlineSmall: AppStyle.title,
titleLarge: AppStyle.headTitle2,
).titleLarge,
),
scaffoldBackgroundColor: const Color(0xFF121212),
cardColor: const Color(0xFF1E1E1E),
dividerColor: Colors.white10,
);