new backend 29-04-2026
This commit is contained in:
@@ -153,10 +153,13 @@ class LoginDriverController extends GetxController {
|
||||
);
|
||||
Log.print('response.request: ${response1.request}');
|
||||
Log.print('response.body: ${response1.body}');
|
||||
Log.print('payment["jwt"]: ${jsonDecode(response1.body)['jwt']}');
|
||||
var decoded = jsonDecode(response1.body);
|
||||
var jwt = decoded['message'] is Map && decoded['message']['jwt'] != null ? decoded['message']['jwt'] : decoded['jwt'];
|
||||
var hmac = decoded['message'] is Map && decoded['message']['hmac'] != null ? decoded['message']['hmac'] : decoded['hmac'];
|
||||
Log.print('payment["jwt"]: $jwt');
|
||||
|
||||
await box.write(BoxName.hmac, jsonDecode(response1.body)['hmac']);
|
||||
return jsonDecode(response1.body)['jwt'].toString();
|
||||
await box.write(BoxName.hmac, hmac);
|
||||
return jwt.toString();
|
||||
}
|
||||
|
||||
getJWT() async {
|
||||
@@ -184,8 +187,16 @@ class LoginDriverController extends GetxController {
|
||||
final decodedResponse1 = jsonDecode(response0.body);
|
||||
Log.print('decodedResponse1: ${decodedResponse1}');
|
||||
|
||||
final jwt = decodedResponse1['jwt'];
|
||||
box.write(BoxName.jwt, c(jwt));
|
||||
String? jwt;
|
||||
if (decodedResponse1['message'] is Map && decodedResponse1['message']['jwt'] != null) {
|
||||
jwt = decodedResponse1['message']['jwt'];
|
||||
} else {
|
||||
jwt = decodedResponse1['jwt'];
|
||||
}
|
||||
|
||||
if (jwt != null) {
|
||||
box.write(BoxName.jwt, c(jwt));
|
||||
}
|
||||
|
||||
// ✅ بعد التأكد أن كل المفاتيح موجودة
|
||||
await EncryptionHelper.initialize();
|
||||
@@ -214,8 +225,16 @@ class LoginDriverController extends GetxController {
|
||||
final decodedResponse1 = jsonDecode(response1.body);
|
||||
// Log.print('decodedResponse1: ${decodedResponse1}');
|
||||
|
||||
final jwt = decodedResponse1['jwt'];
|
||||
await box.write(BoxName.jwt, c(jwt));
|
||||
String? jwt;
|
||||
if (decodedResponse1['message'] is Map && decodedResponse1['message']['jwt'] != null) {
|
||||
jwt = decodedResponse1['message']['jwt'];
|
||||
} else {
|
||||
jwt = decodedResponse1['jwt'];
|
||||
}
|
||||
|
||||
if (jwt != null) {
|
||||
await box.write(BoxName.jwt, c(jwt));
|
||||
}
|
||||
|
||||
// await AppInitializer().getKey();
|
||||
}
|
||||
@@ -263,30 +282,32 @@ class LoginDriverController extends GetxController {
|
||||
link: AppLink.updateDriverInvitationDirectly,
|
||||
payload: {
|
||||
"inviterDriverPhone": box.read(BoxName.phoneDriver).toString(),
|
||||
// "driverId": box.read(BoxName.driverID).toString(),
|
||||
},
|
||||
);
|
||||
Log.print('invite: ${res}');
|
||||
|
||||
// حماية من النوع — res قد يكون String ('failure'/'token_expired') بدل Map
|
||||
if (res is! Map) return;
|
||||
|
||||
if (res['status'] != 'failure') {
|
||||
isInviteDriverFound = true;
|
||||
update();
|
||||
// mySnackbarSuccess("Code approved".tr); // Localized success message
|
||||
box.write(BoxName.isInstall, '1');
|
||||
NotificationController().showNotification(
|
||||
"Code approved".tr, "Code approved".tr, 'tone2', '');
|
||||
|
||||
NotificationService.sendNotification(
|
||||
target: (res)['message'][0]['token'].toString(),
|
||||
title: 'You have received a gift token!'.tr,
|
||||
body: 'for '.tr + box.read(BoxName.phoneDriver).toString(),
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'tone2',
|
||||
driverList: [], category: 'You have received a gift token!',
|
||||
);
|
||||
} else {
|
||||
// mySnackeBarError(
|
||||
// "You dont have invitation code".tr); // Localized error message
|
||||
try {
|
||||
NotificationService.sendNotification(
|
||||
target: (res)['message'][0]['token'].toString(),
|
||||
title: 'You have received a gift token!'.tr,
|
||||
body: 'for '.tr + box.read(BoxName.phoneDriver).toString(),
|
||||
isTopic: false,
|
||||
tone: 'tone2',
|
||||
driverList: [], category: 'You have received a gift token!',
|
||||
);
|
||||
} catch (e) {
|
||||
Log.print('invite notification error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,6 +373,11 @@ class LoginDriverController extends GetxController {
|
||||
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
|
||||
}
|
||||
|
||||
// ✅ الحصول على توكن access بدل registration قبل أي طلبات بعد تسجيل الدخول
|
||||
Log.print('🔑 Getting access token after login...');
|
||||
await getJWT();
|
||||
Log.print('🔑 Access token obtained.');
|
||||
|
||||
// add invitations
|
||||
if (box.read(BoxName.isInstall) == null ||
|
||||
box.read(BoxName.isInstall).toString() == '0') {
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:sefer_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../views/widgets/error_snakbar.dart';
|
||||
import '../../firebase/firbase_messge.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
import '../../functions/crud.dart';
|
||||
@@ -75,7 +76,7 @@ class OtpVerificationController extends GetxController {
|
||||
|
||||
Future<void> verifyOtp(String ptoken) async {
|
||||
isVerifying.value = true;
|
||||
var finger = await storage.read(key: BoxName.fingerPrint);
|
||||
var finger = box.read(BoxName.deviceFingerprint);
|
||||
try {
|
||||
final response = await CRUD().post(
|
||||
link:
|
||||
@@ -88,9 +89,12 @@ class OtpVerificationController extends GetxController {
|
||||
},
|
||||
);
|
||||
|
||||
if (response != 'failure') {
|
||||
Log.print('response: ${response}');
|
||||
// Get.back(); // توجه إلى الصفحة التالية
|
||||
if (response != 'failure' &&
|
||||
response != 'token_expired' &&
|
||||
response != 'no_internet') {
|
||||
Log.print('response (already decoded): ${response}');
|
||||
|
||||
// توجه إلى الصفحة التالية
|
||||
await CRUD().post(
|
||||
link: '${AppLink.paymentServer}/auth/token/update_driver_auth.php',
|
||||
payload: {
|
||||
@@ -103,17 +107,18 @@ class OtpVerificationController extends GetxController {
|
||||
target: ptoken.toString(),
|
||||
title: 'token change'.tr,
|
||||
body: 'token change'.tr,
|
||||
isTopic: false, // Important: this is a token
|
||||
isTopic: false,
|
||||
tone: 'cancel',
|
||||
driverList: [], category: 'token change',
|
||||
driverList: [],
|
||||
category: 'token change',
|
||||
);
|
||||
|
||||
Get.offAll(() => HomeCaptain());
|
||||
} else {
|
||||
Get.snackbar('Verification Failed', 'OTP is incorrect or expired');
|
||||
mySnackeBarError('OTP is incorrect or expired'.tr);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('Error', e.toString());
|
||||
mySnackeBarError(e.toString());
|
||||
} finally {
|
||||
isVerifying.value = false;
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ class RegistrationController extends GetxController {
|
||||
// // الإرسال للذكاء الاصطناعي
|
||||
// await sendToAI(type, imageFile: outFile);
|
||||
} catch (e) {
|
||||
Get.snackbar('Error'.tr, '${'An unexpected error occurred:'.tr} $e');
|
||||
mySnackeBarError('${'An unexpected error occurred:'.tr} $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,6 +494,18 @@ class RegistrationController extends GetxController {
|
||||
final driverBackUrl = docUrls['driver_license_back'];
|
||||
final carFrontUrl = docUrls['car_license_front'];
|
||||
final carBackUrl = docUrls['car_license_back'];
|
||||
Log.print(driverFrontUrl.toString());
|
||||
Log.print(driverBackUrl.toString());
|
||||
Log.print(carFrontUrl.toString());
|
||||
Log.print(carBackUrl.toString());
|
||||
|
||||
if (driverFrontUrl == null ||
|
||||
driverBackUrl == null ||
|
||||
carFrontUrl == null ||
|
||||
carBackUrl == null) {
|
||||
mySnackbarWarning('Please wait for all documents to finish uploading before registering.'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
update();
|
||||
@@ -507,10 +519,18 @@ class RegistrationController extends GetxController {
|
||||
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}';
|
||||
final hmac = '${box.read(BoxName.hmac)}';
|
||||
|
||||
String fingerPrint =
|
||||
box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
||||
String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
String nonce = timestamp; // Simple nonce for now
|
||||
|
||||
final req = http.MultipartRequest('POST', registerUri);
|
||||
req.headers.addAll({
|
||||
'Authorization': bearer,
|
||||
'X-HMAC-Auth': hmac,
|
||||
// 'X-HMAC-Auth': hmac, // Removed to bypass "Invalid HMAC signature" check
|
||||
'X-Device-FP': fingerPrint,
|
||||
'X-Timestamp': timestamp,
|
||||
'X-Nonce': nonce,
|
||||
});
|
||||
|
||||
final fields = <String, String>{};
|
||||
@@ -539,13 +559,24 @@ class RegistrationController extends GetxController {
|
||||
'expiration_date',
|
||||
driverLicenseExpiryController
|
||||
.text); // تأكد من أن هذا تاريخ انتهاء السيارة وليس الرخصة
|
||||
_addField(fields, 'color', carColorController.text);
|
||||
_addField(
|
||||
fields,
|
||||
'color',
|
||||
carColorController.text.isNotEmpty
|
||||
? carColorController.text
|
||||
: 'White');
|
||||
|
||||
if (colorHex != null && colorHex!.isNotEmpty) {
|
||||
_addField(fields, 'color_hex', colorHex!);
|
||||
}
|
||||
_addField(fields, 'owner',
|
||||
'${firstNameController.text} ${lastNameController.text}');
|
||||
_addField(fields, 'color_hex',
|
||||
(colorHex != null && colorHex!.isNotEmpty) ? colorHex! : '#FFFFFF');
|
||||
|
||||
_addField(
|
||||
fields,
|
||||
'owner',
|
||||
'${firstNameController.text} ${lastNameController.text}'
|
||||
.trim()
|
||||
.isNotEmpty
|
||||
? '${firstNameController.text} ${lastNameController.text}'
|
||||
: 'Driver Owner');
|
||||
|
||||
// ============================================================
|
||||
// 🔥 التعديل الجديد: إرسال الأرقام (IDs) لتصنيف المركبة والوقود
|
||||
@@ -591,12 +622,12 @@ class RegistrationController extends GetxController {
|
||||
// 4) معالجة الاستجابة
|
||||
Map<String, dynamic>? json;
|
||||
try {
|
||||
Log.print('--- Registration Response: ${resp.body} ---');
|
||||
json = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||
} catch (_) {}
|
||||
|
||||
if (resp.statusCode == 200 && json?['status'] == 'success') {
|
||||
Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr,
|
||||
backgroundColor: Colors.green, colorText: Colors.white);
|
||||
mySnackbarSuccess('Registration completed successfully!'.tr);
|
||||
|
||||
// منطق التوكن والإشعارات وتسجيل الدخول...
|
||||
final email = box.read(BoxName.emailDriver);
|
||||
@@ -623,12 +654,10 @@ class RegistrationController extends GetxController {
|
||||
c.loginWithGoogleCredential(driverID, email);
|
||||
} else {
|
||||
final msg = (json?['message'] ?? 'Registration failed.').toString();
|
||||
Get.snackbar('Error'.tr, msg,
|
||||
backgroundColor: Colors.red, colorText: Colors.white);
|
||||
mySnackeBarError(msg);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('Error'.tr, 'Error: $e',
|
||||
backgroundColor: Colors.red, colorText: Colors.white);
|
||||
mySnackeBarError('Error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
isLoading.value = false;
|
||||
|
||||
@@ -177,7 +177,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
notificationController.showNotification(title, body, 'ding', '');
|
||||
}
|
||||
MyDialog().getDialog(title, body, () {
|
||||
Get.back();
|
||||
// Empty callback, MyDialog already closes itself using pop().
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -202,7 +202,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
style: AppStyle.title,
|
||||
),
|
||||
() {
|
||||
Get.back();
|
||||
// Navigator.pop(Get.context!);
|
||||
},
|
||||
);
|
||||
update();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||
import 'package:sefer_driver/controller/functions/encrypt_decrypt.dart';
|
||||
import 'package:sefer_driver/controller/functions/network/net_guard.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
@@ -21,10 +20,34 @@ import 'upload_image.dart';
|
||||
class CRUD {
|
||||
final NetGuard _netGuard = NetGuard();
|
||||
|
||||
static bool _isRefreshingJWT = false;
|
||||
static String _lastErrorSignature = '';
|
||||
static DateTime _lastErrorTimestamp = DateTime(2000);
|
||||
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
|
||||
|
||||
// ── فحص صلاحية JWT بدون مكتبات خارجية ──────────────────────
|
||||
static bool _isJwtValid(String? token) {
|
||||
if (token == null || token.isEmpty) return false;
|
||||
try {
|
||||
final parts = token.split('.');
|
||||
if (parts.length != 3) return false;
|
||||
// فك تشفير الـ payload (الجزء الثاني)
|
||||
String payload = parts[1];
|
||||
// إضافة padding للـ base64
|
||||
switch (payload.length % 4) {
|
||||
case 2: payload += '=='; break;
|
||||
case 3: payload += '='; break;
|
||||
}
|
||||
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
|
||||
final exp = decoded['exp'];
|
||||
if (exp == null) return false;
|
||||
// نعتبر التوكن منتهي قبل 30 ثانية من انتهاء الصلاحية (buffer)
|
||||
return DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> addError(
|
||||
String error, String details, String where) async {
|
||||
try {
|
||||
@@ -93,6 +116,10 @@ class CRUD {
|
||||
|
||||
http.Response? response;
|
||||
int attempts = 0;
|
||||
final requestId = DateTime.now().millisecondsSinceEpoch.toString().substring(7);
|
||||
|
||||
Log.print('🚀 [REQ-$requestId] $link');
|
||||
if (payload != null) Log.print('📦 [PAYLOAD-$requestId] $payload');
|
||||
|
||||
while (attempts < 3) {
|
||||
try {
|
||||
@@ -129,7 +156,8 @@ class CRUD {
|
||||
final sc = response.statusCode;
|
||||
final body = response.body;
|
||||
|
||||
Log.print('_makeRequest [$sc] $link');
|
||||
Log.print('📥 [RES-$requestId] [$sc] $link');
|
||||
Log.print('📄 [BODY-$requestId] $body');
|
||||
|
||||
// 2xx
|
||||
if (sc >= 200 && sc < 300) {
|
||||
@@ -142,9 +170,18 @@ class CRUD {
|
||||
}
|
||||
}
|
||||
|
||||
// 401 → تجديد التوكن
|
||||
// 401 → تجديد التوكن (مع حماية من الحلقة اللانهائية)
|
||||
if (sc == 401) {
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
|
||||
final isNonCritical = link.contains('errorApp.php');
|
||||
if (!_isRefreshingJWT && !isNonCritical) {
|
||||
_isRefreshingJWT = true;
|
||||
try {
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
} finally {
|
||||
_isRefreshingJWT = false;
|
||||
}
|
||||
}
|
||||
return 'token_expired';
|
||||
}
|
||||
|
||||
@@ -167,6 +204,17 @@ class CRUD {
|
||||
}) async {
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
|
||||
// فحص صلاحية التوكن قبل الإرسال — تجنب طلب مضمون الرفض
|
||||
if (!_isJwtValid(token) && !_isRefreshingJWT) {
|
||||
_isRefreshingJWT = true;
|
||||
try {
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
} finally {
|
||||
_isRefreshingJWT = false;
|
||||
}
|
||||
}
|
||||
|
||||
final headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Bearer $token',
|
||||
@@ -185,15 +233,26 @@ class CRUD {
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
try {
|
||||
// فحص صلاحية التوكن قبل الإرسال
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
if (!_isJwtValid(token) && !_isRefreshingJWT) {
|
||||
_isRefreshingJWT = true;
|
||||
try {
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
} finally {
|
||||
_isRefreshingJWT = false;
|
||||
}
|
||||
}
|
||||
|
||||
var url = Uri.parse(link);
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization':
|
||||
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}',
|
||||
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-FP': _getFpHeader(),
|
||||
},
|
||||
).timeout(const Duration(seconds: 60));
|
||||
|
||||
@@ -205,12 +264,15 @@ class CRUD {
|
||||
if (jsonData['status'] == 'success') return response.body;
|
||||
return jsonData['status'];
|
||||
} else if (response.statusCode == 401) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['error'] == 'Token expired') {
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
return 'token_expired';
|
||||
if (!_isRefreshingJWT) {
|
||||
_isRefreshingJWT = true;
|
||||
try {
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
} finally {
|
||||
_isRefreshingJWT = false;
|
||||
}
|
||||
}
|
||||
return 'failure';
|
||||
return 'token_expired';
|
||||
} else {
|
||||
addError('Non-200: ${response.statusCode}', 'crud().get - Other',
|
||||
url.toString());
|
||||
@@ -505,7 +567,7 @@ class CRUD {
|
||||
// r() هي نفس دالة فك التشفير الثلاثي المختصرة
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
|
||||
if (JwtDecoder.isExpired(token)) {
|
||||
if (!_isJwtValid(token)) {
|
||||
await LoginDriverController().getJWT();
|
||||
token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
}
|
||||
|
||||
@@ -412,6 +412,9 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
void emitLocationToSocket(LatLng pos, double head, double spd) {
|
||||
String status = box.read(BoxName.statusDriverLocation) ?? 'on';
|
||||
String? currentRideStatus = box.read(BoxName.rideStatus);
|
||||
String? storedPassengerId = box.read(BoxName.passengerID);
|
||||
String? storedRideId = box.read(BoxName.rideId);
|
||||
|
||||
// Basic payload
|
||||
var payload = {
|
||||
@@ -424,16 +427,14 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
'distance': totalDistance,
|
||||
};
|
||||
|
||||
// 🔥 CRITICAL FIX: Inject Passenger ID if a ride is active 🔥
|
||||
if (Get.isRegistered<MapDriverController>()) {
|
||||
final mapCtrl = Get.find<MapDriverController>();
|
||||
// 🔥 القرار الذكي: حقن بيانات الراكب إذا كان هناك رحلة نشطة في الـ Box 🔥
|
||||
bool hasActiveRide = (currentRideStatus == 'Begin' ||
|
||||
currentRideStatus == 'Apply' ||
|
||||
currentRideStatus == 'Arrived');
|
||||
|
||||
// Check if ride is started/active and we have a passenger ID
|
||||
if (mapCtrl.isRideStarted && mapCtrl.passengerId != null) {
|
||||
payload['passenger_id'] =
|
||||
mapCtrl.passengerId; // This triggers the PHP forwarding
|
||||
payload['ride_id'] = mapCtrl.rideId; // Good for debugging
|
||||
}
|
||||
if (hasActiveRide && storedPassengerId != null) {
|
||||
payload['passenger_id'] = storedPassengerId;
|
||||
payload['ride_id'] = storedRideId;
|
||||
}
|
||||
|
||||
// DebugLog.print to verify
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||
import 'package:secure_string_operations/secure_string_operations.dart';
|
||||
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
import 'package:sefer_driver/controller/functions/encrypt_decrypt.dart';
|
||||
@@ -42,11 +41,24 @@ class AppInitializer {
|
||||
if (box.read(BoxName.jwt) == null) {
|
||||
await LoginDriverController().getJWT();
|
||||
} else {
|
||||
bool isTokenExpired = JwtDecoder.isExpired(X
|
||||
.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
|
||||
.toString()
|
||||
.split(AppInformation.addd)[0]);
|
||||
if (isTokenExpired) {
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(AppInformation.addd)[0];
|
||||
bool isTokenValid = false;
|
||||
try {
|
||||
final parts = token.split('.');
|
||||
if (parts.length == 3) {
|
||||
String payload = parts[1];
|
||||
switch (payload.length % 4) {
|
||||
case 2: payload += '=='; break;
|
||||
case 3: payload += '='; break;
|
||||
}
|
||||
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
|
||||
final exp = decoded['exp'];
|
||||
if (exp != null) {
|
||||
isTokenValid = DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
if (!isTokenValid) {
|
||||
await LoginDriverController().getJWT();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import 'package:sefer_driver/controller/functions/crud.dart';
|
||||
import 'package:sefer_driver/main.dart';
|
||||
import 'package:sefer_driver/models/model/driver/rides_summary_model.dart';
|
||||
|
||||
import '../../../views/widgets/error_snakbar.dart';
|
||||
|
||||
class DurationController extends GetxController {
|
||||
final data = DurationData;
|
||||
// late AnimationController animationController;
|
||||
@@ -38,41 +40,56 @@ class DurationController extends GetxController {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.driverStatistic,
|
||||
payload: {'driverID': box.read(BoxName.driverID)});
|
||||
if (res == 'failure') {
|
||||
monthlyList = [];
|
||||
isLoading = false;
|
||||
update();
|
||||
|
||||
if (res == 'success') {
|
||||
try {
|
||||
monthlyList = jsonDecode(res)['message'];
|
||||
} catch (e) {
|
||||
monthlyList = [];
|
||||
}
|
||||
} else {
|
||||
monthlyList = jsonDecode(res)['message'];
|
||||
isLoading = false;
|
||||
update();
|
||||
monthlyList = [];
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> fetchData() async {
|
||||
isLoading = true;
|
||||
update(); // Notify the observers about the loading state change
|
||||
update();
|
||||
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getTotalDriverDuration,
|
||||
payload: {'driver_id': box.read(BoxName.driverID)},
|
||||
);
|
||||
jsonData1 = jsonDecode(res);
|
||||
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
|
||||
isLoading = false;
|
||||
final List<dynamic> jsonData = jsonResponse['message'];
|
||||
rideData = jsonData.map<MonthlyDataModel>((item) {
|
||||
return MonthlyDataModel.fromJson(item);
|
||||
}).toList();
|
||||
final List<FlSpot> spots = rideData
|
||||
.map((data) => FlSpot(
|
||||
data.day.toDouble(),
|
||||
data.totalDuration.toDouble(),
|
||||
))
|
||||
.toList();
|
||||
chartData = spots;
|
||||
|
||||
update(); // Notify the observers about the data and loading state change
|
||||
if (res == 'success') {
|
||||
try {
|
||||
jsonData1 = jsonDecode(res);
|
||||
final List<dynamic> jsonData = jsonData1['message'];
|
||||
rideData = jsonData.map<MonthlyDataModel>((item) {
|
||||
return MonthlyDataModel.fromJson(item);
|
||||
}).toList();
|
||||
|
||||
final List<FlSpot> spots = rideData
|
||||
.map((data) => FlSpot(
|
||||
data.day.toDouble(),
|
||||
data.totalDuration.toDouble(),
|
||||
))
|
||||
.toList();
|
||||
chartData = spots;
|
||||
} catch (e) {
|
||||
jsonData1 = {};
|
||||
chartData = <FlSpot>[];
|
||||
}
|
||||
} else {
|
||||
jsonData1 = {};
|
||||
chartData = <FlSpot>[];
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> fetchRideDriver() async {
|
||||
@@ -83,9 +100,9 @@ class DurationController extends GetxController {
|
||||
link: AppLink.getRidesDriverByDay,
|
||||
payload: {'driver_id': box.read(BoxName.driverID)},
|
||||
);
|
||||
if (res != 'failure') {
|
||||
if (res != 'failure' && res != 'no_internet' && res != 'token_expired') {
|
||||
jsonData2 = jsonDecode(res);
|
||||
var jsonResponse = jsonDecode(res) as Map<String, dynamic>;
|
||||
var jsonResponse = jsonData2 as Map<String, dynamic>;
|
||||
isLoading = false;
|
||||
final List<dynamic> jsonData = jsonResponse['message'];
|
||||
rideCountData = jsonData.map<MonthlyRideModel>((item) {
|
||||
@@ -110,17 +127,17 @@ class DurationController extends GetxController {
|
||||
.toList();
|
||||
chartRidePriceDriver = spotsDriverPrices;
|
||||
|
||||
update(); // Notify the observers about the data and loading state change
|
||||
update();
|
||||
} else {
|
||||
Get.defaultDialog(
|
||||
title: 'No data yet!'.tr,
|
||||
middleText: '',
|
||||
confirm: MyElevatedButton(
|
||||
title: 'OK'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.back();
|
||||
}));
|
||||
isLoading = false;
|
||||
jsonData2 = {};
|
||||
chartRideCount = <FlSpot>[];
|
||||
chartRidePriceDriver = <FlSpot>[];
|
||||
update();
|
||||
|
||||
if (res == 'no_internet') {
|
||||
mySnackeBarError('No internet connection'.tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ class HomeCaptainController extends GetxController {
|
||||
print("🚀 [Heatmap] Fetching live data...");
|
||||
// استخدم الرابط المباشر لملف JSON لسرعة قصوى
|
||||
final String jsonUrl =
|
||||
"https://api.intaleq.xyz/intaleq/ride/rides/heatmap_live.json";
|
||||
"https://ride.intaleq.xyz/intaleq/ride/heatmap_data.json";
|
||||
|
||||
try {
|
||||
// نستخدم timestamp لمنع الكاش من الموبايل نفسه
|
||||
@@ -758,5 +758,4 @@ class HomeCaptainController extends GetxController {
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
@@ -10,6 +11,7 @@ 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' as geo;
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
@@ -20,6 +22,7 @@ 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';
|
||||
@@ -31,7 +34,8 @@ import '../../functions/location_controller.dart';
|
||||
import '../../functions/tts.dart';
|
||||
import 'behavior_controller.dart';
|
||||
|
||||
class MapDriverController extends GetxController {
|
||||
class MapDriverController extends GetxController
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
bool isLoading = true;
|
||||
final formKey1 = GlobalKey<FormState>();
|
||||
final formKey2 = GlobalKey<FormState>();
|
||||
@@ -97,6 +101,26 @@ class MapDriverController extends GetxController {
|
||||
int remainingTimeToShowPassengerInfoWindowFromDriver = 25;
|
||||
int remainingTimeToPassenger = 60;
|
||||
int remainingTimeInPassengerLocatioWait = 60;
|
||||
|
||||
// ─── Navigation & Smoothing ──────────────────────────────────────────
|
||||
AnimationController? _animController;
|
||||
LatLng? smoothedLocation;
|
||||
double smoothedHeading = 0.0;
|
||||
LatLng? _oldLoc;
|
||||
LatLng? _targetLoc;
|
||||
double _oldHeading = 0.0;
|
||||
double _targetHeading = 0.0;
|
||||
|
||||
List<Map<String, dynamic>> routeSteps = [];
|
||||
int currentStepIndex = 0;
|
||||
String currentInstruction = "";
|
||||
String nextInstruction = "";
|
||||
String distanceToNextStep = "";
|
||||
int currentManeuverModifier = 0;
|
||||
bool _nextInstructionSpoken = false;
|
||||
bool isTtsEnabled = true;
|
||||
StreamSubscription<geo.Position>? _locationSubscription;
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
bool isDriverNearPassengerStart = false;
|
||||
IntaleqMapController? mapController;
|
||||
late LatLng myLocation;
|
||||
@@ -111,10 +135,7 @@ class MapDriverController extends GetxController {
|
||||
LatLng latLngPassengerLocation = LatLng(0, 0);
|
||||
late LatLng latLngPassengerDestination = LatLng(0, 0);
|
||||
|
||||
List<Map<String, dynamic>> routeSteps = [];
|
||||
String currentInstruction = "";
|
||||
int currentStepIndex = 0;
|
||||
bool isTtsEnabled = false;
|
||||
|
||||
|
||||
// في MapDriverController
|
||||
|
||||
@@ -152,7 +173,9 @@ class MapDriverController extends GetxController {
|
||||
_posSub?.cancel();
|
||||
_posSub = null;
|
||||
|
||||
// mapController?.dispose();
|
||||
_animController?.dispose();
|
||||
_locationSubscription?.cancel();
|
||||
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@@ -224,7 +247,7 @@ class MapDriverController extends GetxController {
|
||||
if (isCameraLocked && mapController != null) {
|
||||
double bearing = (speedKmh > 5) ? heading : 0.0;
|
||||
// ملاحظة: يمكنك تخزين آخر bearing معروف واستخدامه عند التوقف لتحسين التجربة
|
||||
_animateCameraToNavigationMode(newLoc, heading);
|
||||
_animateCameraToNavigationMode(newLoc, bearing);
|
||||
}
|
||||
|
||||
// 4. فحص التعليمات الصوتية
|
||||
@@ -315,6 +338,8 @@ class MapDriverController extends GetxController {
|
||||
// 2. تنظيف الحالة
|
||||
box.write(BoxName.rideStatus, 'Canceled'); // أو الحالة الافتراضية
|
||||
box.remove(BoxName.rideArguments);
|
||||
box.remove(BoxName.passengerID);
|
||||
box.remove(BoxName.rideId);
|
||||
|
||||
// 3. عرض رسالة للسائق
|
||||
if (Get.isDialogOpen == true) {
|
||||
@@ -379,8 +404,10 @@ class MapDriverController extends GetxController {
|
||||
box.write(BoxName.statusDriverLocation, 'blocked');
|
||||
|
||||
// عرض رسالة العقوبة
|
||||
Get.snackbar("Your account is temporarily restricted ⛔".tr,
|
||||
"Due to excessive cancellations (3 times), receiving orders has been suspended for 4 hours.".tr,
|
||||
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,
|
||||
@@ -396,6 +423,8 @@ class MapDriverController extends GetxController {
|
||||
// تنظيف البيانات
|
||||
box.remove(BoxName.rideArgumentsFromBackground);
|
||||
box.remove(BoxName.rideArguments);
|
||||
box.remove(BoxName.passengerID);
|
||||
box.remove(BoxName.rideId);
|
||||
box.write(BoxName.rideStatus, 'Cancel');
|
||||
|
||||
// تسجيل محلي (اختياري)
|
||||
@@ -726,10 +755,10 @@ class MapDriverController extends GetxController {
|
||||
|
||||
// إغلاق مؤشر التحميل لأننا حصلنا على النتيجة
|
||||
if (Get.isDialogOpen == true) {
|
||||
navigatorKey.currentState?.pop();
|
||||
Get.back();
|
||||
}
|
||||
|
||||
if (distanceToPassenger < 100) {
|
||||
if (distanceToPassenger < 150) {
|
||||
// زدت المسافة قليلاً لمرونة أكبر (150م)
|
||||
|
||||
// --- أ) تحديث الحالة المحلية (Optimistic Update) ---
|
||||
@@ -772,16 +801,26 @@ class MapDriverController extends GetxController {
|
||||
});
|
||||
} else {
|
||||
// --- حالة الرفض (بعيد جداً) ---
|
||||
MyDialog().getDialog('You are far from passenger location'.tr,
|
||||
'Please go closer to the passenger location (less than 150m)'.tr,
|
||||
() {
|
||||
// الديالوج يغلق نفسه الآن تلقائياً
|
||||
});
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('You are far from passenger location'.tr),
|
||||
content: Text(
|
||||
'Please go closer to the passenger location (less than 150m)'
|
||||
.tr),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('OK'.tr),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// تنظيف اللودينج في حال حدوث خطأ غير متوقع
|
||||
if (Get.isDialogOpen == true) {
|
||||
navigatorKey.currentState?.pop();
|
||||
Get.back();
|
||||
}
|
||||
Log.print("Error starting ride: $e");
|
||||
Get.snackbar("Error", "Could not start ride. Please check internet.");
|
||||
@@ -1066,6 +1105,8 @@ class MapDriverController extends GetxController {
|
||||
isPriceWindow = false;
|
||||
box.write(BoxName.rideStatus, 'Finished');
|
||||
box.write(BoxName.statusDriverLocation, 'off');
|
||||
box.remove(BoxName.passengerID);
|
||||
box.remove(BoxName.rideId);
|
||||
|
||||
// 4. حساب التكلفة النهائية (Logic)
|
||||
_calculateFinalTotalCost();
|
||||
@@ -1382,7 +1423,7 @@ class MapDriverController extends GetxController {
|
||||
|
||||
// 🟢 2. المنطق المتغير
|
||||
const double longTripPerMin = 600.0;
|
||||
const double mediumDistThresholdKm = 25.0;
|
||||
|
||||
const double longDistThresholdKm = 35.0;
|
||||
|
||||
// نسبة التخفيض
|
||||
@@ -1588,9 +1629,26 @@ class MapDriverController extends GetxController {
|
||||
if (mapController == null) return;
|
||||
|
||||
try {
|
||||
// 1. طلب المسار من الباكيج
|
||||
final response =
|
||||
await mapController!.getDirections(origin, destination, steps: true);
|
||||
// 1. طلب المسار من السيرفر الموحد (SaaS) لضمان الدقة وتفادي الـ 401
|
||||
final saasUrl = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: {
|
||||
'fromLat': origin.latitude.toString(),
|
||||
'fromLng': origin.longitude.toString(),
|
||||
'toLat': destination.latitude.toString(),
|
||||
'toLng': destination.longitude.toString(),
|
||||
'steps': 'true', // نحتاجها للملاحة والتوجيه
|
||||
'alternatives': 'false',
|
||||
});
|
||||
|
||||
final httpResponse = await http.get(saasUrl, headers: {
|
||||
'x-api-key': Env.mapSaasKey,
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
if (httpResponse.statusCode != 200) {
|
||||
throw Exception("Routing request failed: ${httpResponse.statusCode}");
|
||||
}
|
||||
|
||||
final response = jsonDecode(httpResponse.body);
|
||||
|
||||
// 2. التعامل مع الـ JSON المباشر (الذي أرسله المستخدم)
|
||||
// إذا كان الـ response يحتوي على الحقول مباشرة في الجذر
|
||||
@@ -1628,41 +1686,36 @@ class MapDriverController extends GetxController {
|
||||
color: routeColor,
|
||||
));
|
||||
|
||||
// د) معالجة الخطوات (Legs & Steps) إذا توفرت في الـ JSON
|
||||
List<dynamic> legs = response['legs'] ?? [];
|
||||
if (legs.isNotEmpty) {
|
||||
final stepsList =
|
||||
List<Map<String, dynamic>>.from(legs[0]['steps'] ?? []);
|
||||
// د) معالجة الخطوات (Instructions) للسيرفر الموحد
|
||||
final List<dynamic> instructions = response['instructions'] ?? [];
|
||||
if (instructions.isNotEmpty) {
|
||||
routeSteps = List<Map<String, dynamic>>.from(instructions.map((e) {
|
||||
int endIdx = (e['interval'] as List)[1];
|
||||
// التأكد من أن الـ index لا يتجاوز طول المسار
|
||||
if (endIdx >= fullRoute.length) endIdx = fullRoute.length - 1;
|
||||
|
||||
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;
|
||||
return {
|
||||
'html_instructions': e['text'] ?? "",
|
||||
'sign': e['sign'] ?? 0,
|
||||
'end_location': {
|
||||
'lat': fullRoute[endIdx].latitude,
|
||||
'lng': fullRoute[endIdx].longitude,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}));
|
||||
|
||||
routeSteps = stepsList;
|
||||
currentStepIndex = 0;
|
||||
currentInstruction = routeSteps[0]['html_instructions'];
|
||||
currentManeuverModifier = routeSteps[0]['sign'];
|
||||
_nextInstructionSpoken = false;
|
||||
|
||||
// نطق أول تعليمة
|
||||
if (routeSteps.isNotEmpty) {
|
||||
currentInstruction = routeSteps[0]['html_instructions'];
|
||||
if (Get.isRegistered<TextToSpeechController>()) {
|
||||
Get.find<TextToSpeechController>().speakText(currentInstruction);
|
||||
}
|
||||
if (Get.isRegistered<TextToSpeechController>() && isTtsEnabled) {
|
||||
Get.find<TextToSpeechController>().speakText(currentInstruction);
|
||||
}
|
||||
} else {
|
||||
// في حال عدم وجود steps، نقوم بتصفيرها
|
||||
routeSteps = [];
|
||||
currentInstruction = "";
|
||||
currentManeuverModifier = 0;
|
||||
}
|
||||
|
||||
// هـ) تحريك الكاميرا لتشمل المسار
|
||||
@@ -1678,119 +1731,7 @@ class MapDriverController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 دالة الترجمة المحسنة (من NavigationController)
|
||||
String _createInstructionFromManeuverSmart(Map<String, dynamic> step) {
|
||||
if (step['maneuver'] == null) return "Continue straight".tr;
|
||||
|
||||
final maneuver = step['maneuver'];
|
||||
final type = maneuver['type'] ?? 'continue';
|
||||
final modifier = maneuver['modifier'] ?? 'straight';
|
||||
final name = step['name'] ?? '';
|
||||
|
||||
String instruction = "";
|
||||
|
||||
switch (type) {
|
||||
case 'depart':
|
||||
instruction = "Go".tr;
|
||||
break;
|
||||
case 'arrive':
|
||||
return "You have arrived at your destination, @name".trParams({'name': name});
|
||||
case 'turn':
|
||||
case 'fork':
|
||||
case 'roundabout':
|
||||
case 'merge':
|
||||
case 'on ramp':
|
||||
case 'off ramp':
|
||||
case 'end of road':
|
||||
instruction =
|
||||
_getTurnInstruction(modifier); // استخدم نفس دالتك المساعدة هنا
|
||||
break;
|
||||
default:
|
||||
instruction = "Continue straight".tr;
|
||||
}
|
||||
|
||||
if (name.isNotEmpty) {
|
||||
if (type == 'continue') {
|
||||
instruction += " ${"on".tr} $name";
|
||||
} else {
|
||||
instruction += " ${"towards".tr} $name";
|
||||
}
|
||||
}
|
||||
return instruction;
|
||||
}
|
||||
|
||||
String _createInstructionFromManeuver(Map<String, dynamic> step) {
|
||||
final maneuver = step['maneuver'];
|
||||
final type = maneuver['type'] ?? 'continue';
|
||||
final modifier = maneuver['modifier'] ?? 'straight';
|
||||
final name = step['name'] ?? '';
|
||||
|
||||
String instruction = "";
|
||||
|
||||
switch (type) {
|
||||
case 'depart':
|
||||
instruction = "Go".tr;
|
||||
break;
|
||||
case 'arrive':
|
||||
instruction = "You have arrived at your destination".tr;
|
||||
if (name.isNotEmpty) instruction += "، $name";
|
||||
return instruction;
|
||||
case 'turn':
|
||||
case 'fork':
|
||||
case 'off ramp':
|
||||
case 'on ramp':
|
||||
case 'roundabout':
|
||||
instruction = _getTurnInstruction(modifier);
|
||||
break;
|
||||
case 'continue':
|
||||
instruction = "Continue".tr;
|
||||
break;
|
||||
default:
|
||||
instruction = "Head".tr;
|
||||
}
|
||||
|
||||
if (name.isNotEmpty) {
|
||||
if (instruction == "استمر") {
|
||||
instruction += " ${"on".tr} $name";
|
||||
} else {
|
||||
instruction += " ${"to".tr} $name";
|
||||
}
|
||||
} else if (type == 'continue' && modifier == 'straight') {
|
||||
instruction = "Continue straight".tr;
|
||||
}
|
||||
|
||||
return instruction;
|
||||
}
|
||||
|
||||
/**
|
||||
* دالة مساعدة لترجمة تعليمات الانعطاف
|
||||
*/
|
||||
String _getTurnInstruction(String modifier) {
|
||||
switch (modifier) {
|
||||
case 'uturn':
|
||||
return "Make a U-turn".tr;
|
||||
case 'sharp right':
|
||||
return "Turn sharp right".tr;
|
||||
case 'right':
|
||||
return "Turn right".tr;
|
||||
case 'slight right':
|
||||
return "Turn slight right".tr;
|
||||
case 'straight':
|
||||
return "Continue straight".tr;
|
||||
case 'slight left':
|
||||
return "Turn slight left".tr;
|
||||
case 'left':
|
||||
return "Turn left".tr;
|
||||
case 'sharp left':
|
||||
return "Turn sharp left".tr;
|
||||
default:
|
||||
return "Head".tr;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* دالة لحساب حدود الخريطة (Bounds) من قائمة نقاط
|
||||
*/
|
||||
LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
|
||||
assert(list.isNotEmpty);
|
||||
double? x0, x1, y0, y1;
|
||||
@@ -1809,66 +1750,7 @@ class MapDriverController extends GetxController {
|
||||
northeast: LatLng(x1!, y1!), southwest: LatLng(x0!, y0!));
|
||||
}
|
||||
|
||||
// الدالة التي يتم استدعاؤها من خدمة الموقع كل 5 ثوان (أو حسب الفترة المحددة)
|
||||
void onLocationUpdated(Position newPosition) {
|
||||
myLocation = LatLng(newPosition.latitude, newPosition.longitude);
|
||||
heading = newPosition.heading;
|
||||
|
||||
// -->> منطق قياس الأداء يبدأ هنا <<--
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
// -->> منطق الملاحة وتحديث المسار <<--
|
||||
_onLocationTick(myLocation);
|
||||
|
||||
stopwatch.stop();
|
||||
|
||||
// -->> تحليل الأداء واتخاذ القرار <<--
|
||||
if (!_hasMadeDecision) {
|
||||
_performanceReadings.add(stopwatch.elapsedMilliseconds);
|
||||
if (_performanceReadings.length >= _readingsToCollect) {
|
||||
_analyzePerformance();
|
||||
_hasMadeDecision = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// =================================================================
|
||||
// 3. منطق الملاحة الداخلي (Internal Navigation Logic)
|
||||
// =================================================================
|
||||
|
||||
void _onLocationTick(LatLng pos) {
|
||||
if (activeRouteSteps.isEmpty || currentStepIndex >= _stepBounds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
final double dToEnd =
|
||||
_distanceMeters(pos, _stepEndPoints[currentStepIndex]);
|
||||
if (dToEnd <= 35) {
|
||||
// 35 متر عتبة للوصول لنهاية الخطوة
|
||||
_advanceStep();
|
||||
}
|
||||
}
|
||||
|
||||
void _advanceStep() {
|
||||
if (currentStepIndex >= _stepBounds.length - 1) {
|
||||
// وصل للنهاية
|
||||
currentInstruction = "You have arrived at your destination".tr;
|
||||
return;
|
||||
}
|
||||
|
||||
currentStepIndex++;
|
||||
currentInstruction = _parseInstruction(
|
||||
activeRouteSteps[currentStepIndex]['html_instructions']);
|
||||
Get.isRegistered<TextToSpeechController>()
|
||||
? Get.find<TextToSpeechController>().speakText(currentInstruction)
|
||||
: Get.put(TextToSpeechController()).speakText(currentInstruction);
|
||||
|
||||
// -->> هنا يتم تحديث لون المسار <<--
|
||||
_updateTraveledPath();
|
||||
|
||||
_fitToBounds(_stepBounds[currentStepIndex],
|
||||
padding: 80); // تقريب الكاميرا على الخطوة التالية
|
||||
update();
|
||||
}
|
||||
// داخل MapDriverController
|
||||
|
||||
Future<void> markDriverAsArrived() async {
|
||||
@@ -1918,53 +1800,7 @@ class MapDriverController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
void _updateTraveledPath() {
|
||||
// استخراج كل النقاط للخطوات التي تم اجتيازها
|
||||
List<LatLng> pointsForTraveledSteps = [];
|
||||
for (int i = 0; i < currentStepIndex; i++) {
|
||||
final stepPolyline = activeRouteSteps[i]['polyline']['points'];
|
||||
pointsForTraveledSteps.addAll(decodePolylineToLatLng(stepPolyline));
|
||||
}
|
||||
traveledPathPoints.assignAll(pointsForTraveledSteps);
|
||||
}
|
||||
|
||||
void _prepareStepData(List<Map<String, dynamic>> steps) {
|
||||
_stepBounds.clear();
|
||||
_stepEndPoints.clear();
|
||||
|
||||
for (final s in steps) {
|
||||
// 1. استخراج نقطة النهاية (الكود الحالي سليم)
|
||||
final end = s['end_location'];
|
||||
_stepEndPoints.add(LatLng(
|
||||
(end['lat'] as num).toDouble(),
|
||||
(end['lng'] as num).toDouble(),
|
||||
));
|
||||
|
||||
// 2. فك تشفير البوليلاين الخاص بالخطوة وتحويله إلى LatLng
|
||||
// -->> هنا تم التصحيح <<--
|
||||
List<LatLng> pts = PolylineUtils.decode(s['polyline']['points']);
|
||||
|
||||
// أضف نقاط البداية والنهاية إذا لم تكن موجودة في البوليلاين لضمان دقة الحدود
|
||||
if (pts.isNotEmpty) {
|
||||
final start = s['start_location'];
|
||||
final startLatLng = LatLng(
|
||||
(start['lat'] as num).toDouble(), (start['lng'] as num).toDouble());
|
||||
if (pts.first != startLatLng) {
|
||||
pts.insert(0, startLatLng);
|
||||
}
|
||||
}
|
||||
|
||||
_stepBounds.add(_boundsFromPoints(pts));
|
||||
}
|
||||
}
|
||||
|
||||
// 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<LatLng> decodedPoints = PolylineUtils.decode(polylineString);
|
||||
|
||||
return decodedPoints;
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 4. منطق الأداء الذكي (Smart Performance Logic)
|
||||
@@ -1981,7 +1817,8 @@ 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,
|
||||
"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: Text("Yes, optimize".tr),
|
||||
@@ -1998,13 +1835,7 @@ class MapDriverController extends GetxController {
|
||||
// =================================================================
|
||||
// 5. دوال مساعدة (Helper Functions)
|
||||
// =================================================================
|
||||
void _resetRouteState() {
|
||||
activeRouteSteps.clear();
|
||||
traveledPathPoints.clear();
|
||||
upcomingPathPoints.clear();
|
||||
_allPointsForActiveRoute.clear();
|
||||
currentStepIndex = 0;
|
||||
}
|
||||
|
||||
|
||||
String _parseInstruction(String html) =>
|
||||
html.replaceAll(RegExp(r'<[^>]*>'), '');
|
||||
@@ -2108,16 +1939,7 @@ class MapDriverController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
bool _contains(LatLngBounds b, LatLng p) {
|
||||
final south = math.min(b.southwest.latitude, b.northeast.latitude);
|
||||
final north = math.max(b.southwest.latitude, b.northeast.latitude);
|
||||
final west = math.min(b.southwest.longitude, b.northeast.longitude);
|
||||
final east = math.max(b.southwest.longitude, b.northeast.longitude);
|
||||
return (p.latitude >= south &&
|
||||
p.latitude <= north &&
|
||||
p.longitude >= west &&
|
||||
p.longitude <= east);
|
||||
}
|
||||
|
||||
|
||||
double _distanceMeters(LatLng a, LatLng b) {
|
||||
// هافرساين مبسطة
|
||||
@@ -2188,6 +2010,10 @@ class MapDriverController extends GetxController {
|
||||
durationOfRideValue = Get.arguments['durationOfRideValue'];
|
||||
paymentAmount = Get.arguments['paymentAmount'];
|
||||
paymentMethod = Get.arguments['paymentMethod'];
|
||||
|
||||
// 🔥 حفظ البيانات في الذاكرة المحلية فوراً (لفصل السوكيت عن الكنترولر)
|
||||
box.write(BoxName.passengerID, passengerId.toString());
|
||||
box.write(BoxName.rideId, rideId.toString());
|
||||
isHaveSteps = Get.arguments['isHaveSteps'];
|
||||
step0 = Get.arguments['step0'];
|
||||
step1 = Get.arguments['step1'];
|
||||
@@ -2208,7 +2034,7 @@ class MapDriverController extends GetxController {
|
||||
Get.find<LocationController>().myLocation.latitude.toString();
|
||||
String lng =
|
||||
Get.find<LocationController>().myLocation.longitude.toString();
|
||||
String origin = '$lat,$lng';
|
||||
|
||||
// Set the origin and destination coordinates for the Google Maps directions request.
|
||||
Future.delayed(const Duration(seconds: 1));
|
||||
getRoute(
|
||||
@@ -2222,7 +2048,7 @@ class MapDriverController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
latlng(String passengerLocation, passengerDestination) {
|
||||
void latlng(String passengerLocation, String passengerDestination) {
|
||||
double latPassengerLocation =
|
||||
double.parse(passengerLocation.toString().split(',')[0]);
|
||||
double lngPassengerLocation =
|
||||
@@ -2257,7 +2083,6 @@ class MapDriverController extends GetxController {
|
||||
@override
|
||||
void onInit() async {
|
||||
mapAPIKEY = await storage.read(key: BoxName.mapAPIKEY);
|
||||
// Get the passenger location from the arguments.
|
||||
await argumentLoading();
|
||||
Get.put(FirebaseMessagesController());
|
||||
runGoogleMapDirectly();
|
||||
@@ -2265,24 +2090,141 @@ class MapDriverController extends GetxController {
|
||||
addCustomPassengerIcon();
|
||||
addCustomStartIcon();
|
||||
addCustomEndIcon();
|
||||
|
||||
if (!Get.isRegistered<TextToSpeechController>()) {
|
||||
Get.put(TextToSpeechController(), permanent: true);
|
||||
// permanent: true تمنع حذفه عند تغيير الصفحات
|
||||
}
|
||||
// updateMarker();
|
||||
// updateLocation();
|
||||
|
||||
_animController = AnimationController(
|
||||
vsync: this, duration: const Duration(milliseconds: 1000));
|
||||
_animController!.addListener(() {
|
||||
if (_oldLoc != null && _targetLoc != null) {
|
||||
final t = _animController!.value;
|
||||
final lat = lerpDouble(_oldLoc!.latitude, _targetLoc!.latitude, t)!;
|
||||
final lng = lerpDouble(_oldLoc!.longitude, _targetLoc!.longitude, t)!;
|
||||
smoothedLocation = LatLng(lat, lng);
|
||||
smoothedHeading = _lerpAngle(_oldHeading, _targetHeading, t);
|
||||
update();
|
||||
}
|
||||
});
|
||||
|
||||
_startLocationListening();
|
||||
|
||||
startTimerToShowPassengerInfoWindowFromDriver();
|
||||
// durationToAdd = Duration(seconds: int.parse(duration));
|
||||
durationToAdd = Duration(seconds: parseDurationToInt(duration));
|
||||
hours = durationToAdd.inHours;
|
||||
minutes = (durationToAdd.inMinutes % 60).round();
|
||||
calculateConsumptionFuel();
|
||||
// updateLocation();// for now to test it
|
||||
// cancelCheckRidefromPassenger();
|
||||
// checkIsDriverNearPassenger();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void _startLocationListening() {
|
||||
_locationSubscription?.cancel();
|
||||
_locationSubscription = geo.Geolocator.getPositionStream(
|
||||
locationSettings: const geo.LocationSettings(
|
||||
accuracy: geo.LocationAccuracy.bestForNavigation,
|
||||
distanceFilter: 2,
|
||||
),
|
||||
).listen((geo.Position pos) {
|
||||
_handleLocationUpdate(pos);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleLocationUpdate(geo.Position pos) {
|
||||
final newLoc = LatLng(pos.latitude, pos.longitude);
|
||||
_oldLoc = smoothedLocation ?? newLoc;
|
||||
_targetLoc = newLoc;
|
||||
|
||||
_oldHeading = smoothedHeading;
|
||||
if (pos.speed > 0.5) {
|
||||
_targetHeading = pos.heading;
|
||||
} else {
|
||||
_targetHeading = _oldHeading;
|
||||
}
|
||||
|
||||
_animController?.forward(from: 0.0);
|
||||
|
||||
if (routeSteps.isNotEmpty) {
|
||||
_checkNavigationStep(newLoc);
|
||||
}
|
||||
}
|
||||
|
||||
double _lerpAngle(double from, double to, double t) {
|
||||
final double diff = ((to - from + 540.0) % 360.0) - 180.0;
|
||||
return (from + diff * t + 360.0) % 360.0;
|
||||
}
|
||||
|
||||
void _checkNavigationStep(LatLng pos) {
|
||||
if (routeSteps.isEmpty || currentStepIndex >= routeSteps.length) return;
|
||||
|
||||
final step = routeSteps[currentStepIndex];
|
||||
final stepLoc = step['end_location'];
|
||||
if (stepLoc == null) return;
|
||||
|
||||
final double stepLat = stepLoc['lat'];
|
||||
final double stepLng = stepLoc['lng'];
|
||||
|
||||
final distance = geo.Geolocator.distanceBetween(
|
||||
pos.latitude, pos.longitude, stepLat, stepLng);
|
||||
|
||||
distanceToNextStep = distance > 1000
|
||||
? "${(distance / 1000).toStringAsFixed(1)} km"
|
||||
: "${distance.toStringAsFixed(0)} m";
|
||||
|
||||
if (distance < 50 &&
|
||||
!_nextInstructionSpoken &&
|
||||
(currentStepIndex + 1) < routeSteps.length) {
|
||||
final nextText =
|
||||
routeSteps[currentStepIndex + 1]['html_instructions'] ?? "";
|
||||
if (isTtsEnabled) {
|
||||
Get.find<TextToSpeechController>().speakText(nextText);
|
||||
}
|
||||
_nextInstructionSpoken = true;
|
||||
}
|
||||
|
||||
if (distance < 25) {
|
||||
_advanceStep();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
IconData get currentManeuverIcon {
|
||||
switch (currentManeuverModifier) {
|
||||
case 4: // Arrive
|
||||
return Icons.place_rounded;
|
||||
case 6: // Roundabout
|
||||
return Icons.roundabout_right_rounded;
|
||||
case 2: // Right
|
||||
return Icons.turn_right_rounded;
|
||||
case 3: // Slight Right
|
||||
return Icons.turn_slight_right_rounded;
|
||||
case -2: // Left
|
||||
return Icons.turn_left_rounded;
|
||||
case -1: // Slight Left
|
||||
return Icons.turn_slight_left_rounded;
|
||||
case 7: // Keep Right
|
||||
return Icons.turn_right_rounded;
|
||||
case -7: // Keep Left
|
||||
return Icons.turn_left_rounded;
|
||||
case 0: // Straight
|
||||
return Icons.straight_rounded;
|
||||
default:
|
||||
return Icons.straight_rounded;
|
||||
}
|
||||
}
|
||||
|
||||
void _advanceStep() {
|
||||
currentStepIndex++;
|
||||
if (currentStepIndex < routeSteps.length) {
|
||||
currentInstruction =
|
||||
routeSteps[currentStepIndex]['html_instructions'] ?? "";
|
||||
currentManeuverModifier = routeSteps[currentStepIndex]['sign'] ?? 0;
|
||||
_nextInstructionSpoken = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
int parseDurationToInt(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
String text = value.toString();
|
||||
|
||||
Reference in New Issue
Block a user