Initial commit for Intaleq Driver
This commit is contained in:
@@ -5,9 +5,8 @@ import 'package:crypto/crypto.dart';
|
||||
import 'dart:math';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:secure_string_operations/secure_string_operations.dart';
|
||||
import 'package:sefer_driver/controller/functions/location_background_controller.dart';
|
||||
import 'package:sefer_driver/views/auth/captin/cards/sms_signup.dart';
|
||||
import 'package:sefer_driver/views/auth/syria/registration_view.dart';
|
||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -21,13 +20,10 @@ import 'package:sefer_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||
import 'package:location/location.dart';
|
||||
|
||||
import '../../../constant/api_key.dart';
|
||||
import '../../../constant/char_map.dart';
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../constant/table_names.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../../views/auth/captin/cards/syrian_card_a_i.dart';
|
||||
import '../../../views/auth/captin/otp_page.dart';
|
||||
import '../../../views/auth/captin/otp_token_page.dart';
|
||||
import '../../../views/auth/syria/pending_driver_page.dart';
|
||||
import '../../firebase/firbase_messge.dart';
|
||||
import '../../functions/encrypt_decrypt.dart';
|
||||
import '../../functions/package_info.dart';
|
||||
@@ -79,11 +75,11 @@ class LoginDriverController extends GetxController {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getTesterApp,
|
||||
payload: {'appPlatform': AppInformation.appName});
|
||||
Log.print('res: ${res}');
|
||||
// Log.print('res: ${res}');
|
||||
if (res != 'failure') {
|
||||
var d = jsonDecode(res);
|
||||
isTest = d['message'][0]['isTest'];
|
||||
Log.print('isTest: ${isTest}');
|
||||
// Log.print('isTest: ${isTest}');
|
||||
box.write(BoxName.isTest, isTest);
|
||||
|
||||
// Log.print('isTest: ${box.read(BoxName.isTest)}');
|
||||
@@ -108,7 +104,8 @@ class LoginDriverController extends GetxController {
|
||||
)
|
||||
});
|
||||
if (res != 'failure') {
|
||||
Get.offAll(() => SyrianCardAI());
|
||||
// Get.offAll(() => SyrianCardAI());
|
||||
Get.offAll(() => RegistrationView());
|
||||
|
||||
// isloading = false;
|
||||
// update();
|
||||
@@ -322,46 +319,53 @@ class LoginDriverController extends GetxController {
|
||||
} else if (int.parse(d['year'].toString()) < 2002) {
|
||||
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
|
||||
}
|
||||
updateAppTester(AppInformation.appName);
|
||||
// updateAppTester(AppInformation.appName);
|
||||
if (d['status'].toString() != 'yet') {
|
||||
var token = await CRUD().get(
|
||||
link: AppLink.getDriverToken,
|
||||
payload: {
|
||||
'captain_id': (box.read(BoxName.driverID)).toString()
|
||||
});
|
||||
|
||||
var token = await CRUD().get(
|
||||
link: AppLink.getDriverToken,
|
||||
payload: {'captain_id': (box.read(BoxName.driverID)).toString()});
|
||||
|
||||
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||
await storage.write(
|
||||
key: BoxName.fingerPrint, value: fingerPrint.toString());
|
||||
// print(jsonDecode(token)['data'][0]['token'].toString());
|
||||
// print(box.read(BoxName.tokenDriver).toString());
|
||||
if (email == '962798583052@intaleqapp.com') {
|
||||
} else {
|
||||
if (token != 'failure') {
|
||||
if ((jsonDecode(token)['data'][0]['token'].toString()) !=
|
||||
box.read(BoxName.tokenDriver).toString()) {
|
||||
await Get.defaultDialog(
|
||||
title: 'Device Change Detected'.tr,
|
||||
middleText: 'Please verify your identity'.tr,
|
||||
textConfirm: 'Verify'.tr,
|
||||
confirmTextColor: Colors.white,
|
||||
onConfirm: () {
|
||||
// Get.back();
|
||||
// انتقل لصفحة OTP الجديدة
|
||||
Get.to(
|
||||
() => OtpVerificationPage(
|
||||
phone: d['phone'].toString(),
|
||||
deviceToken: fingerPrint.toString(),
|
||||
token: token.toString(),
|
||||
ptoken:
|
||||
jsonDecode(token)['data'][0]['token'].toString(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||
await storage.write(
|
||||
key: BoxName.fingerPrint, value: fingerPrint.toString());
|
||||
// print(jsonDecode(token)['data'][0]['token'].toString());
|
||||
// print(box.read(BoxName.tokenDriver).toString());
|
||||
if (email == '962798583052@intaleqapp.com') {
|
||||
} else {
|
||||
if (token != 'failure') {
|
||||
if ((jsonDecode(token)['data'][0]['token'].toString()) !=
|
||||
box.read(BoxName.tokenDriver).toString()) {
|
||||
await Get.defaultDialog(
|
||||
title: 'Device Change Detected'.tr,
|
||||
middleText: 'Please verify your identity'.tr,
|
||||
textConfirm: 'Verify'.tr,
|
||||
confirmTextColor: Colors.white,
|
||||
onConfirm: () {
|
||||
// Get.back();
|
||||
// انتقل لصفحة OTP الجديدة
|
||||
Get.to(
|
||||
() => OtpVerificationPage(
|
||||
phone: d['phone'].toString(),
|
||||
deviceToken: fingerPrint.toString(),
|
||||
token: token.toString(),
|
||||
ptoken:
|
||||
jsonDecode(token)['data'][0]['token'].toString(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Get.offAll(() => HomeCaptain());
|
||||
} else {
|
||||
Get.off(() => DriverVerificationScreen());
|
||||
}
|
||||
|
||||
Get.off(() => HomeCaptain());
|
||||
// Get.off(() => HomeCaptain());
|
||||
} else {
|
||||
Get.offAll(() => PhoneNumberScreen());
|
||||
|
||||
@@ -476,7 +480,8 @@ class LoginDriverController extends GetxController {
|
||||
if (res == 'failure') {
|
||||
//Failure
|
||||
if (box.read(BoxName.phoneVerified).toString() == '1') {
|
||||
Get.offAll(() => SyrianCardAI());
|
||||
// Get.offAll(() => SyrianCardAI());
|
||||
Get.offAll(() => RegistrationView());
|
||||
} else {
|
||||
Get.offAll(() => SmsSignupEgypt());
|
||||
}
|
||||
@@ -551,24 +556,7 @@ class LoginDriverController extends GetxController {
|
||||
'captain_id': box.read(BoxName.driverID).toString(),
|
||||
'fingerPrint': (fingerPrint).toString()
|
||||
});
|
||||
await CRUD().post(
|
||||
link:
|
||||
"${AppLink.seferAlexandriaServer}/ride/firebase/addDriver.php",
|
||||
payload: {
|
||||
'token': box.read(BoxName.tokenDriver),
|
||||
'captain_id':
|
||||
box.read(BoxName.driverID).toString(),
|
||||
'fingerPrint': (fingerPrint).toString()
|
||||
});
|
||||
await CRUD().post(
|
||||
link:
|
||||
"${AppLink.seferGizaServer}/ride/firebase/addDriver.php",
|
||||
payload: {
|
||||
'token': box.read(BoxName.tokenDriver),
|
||||
'captain_id':
|
||||
box.read(BoxName.driverID).toString(),
|
||||
'fingerPrint': (fingerPrint).toString()
|
||||
});
|
||||
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||
import 'package:sefer_driver/views/auth/captin/cards/syrian_card_a_i.dart';
|
||||
import 'package:sefer_driver/print.dart';
|
||||
import 'package:sefer_driver/views/home/on_boarding_page.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../../views/auth/captin/otp_page.dart';
|
||||
import '../../../views/auth/syria/registration_view.dart';
|
||||
|
||||
// --- Helper Class for Phone Authentication ---
|
||||
|
||||
@@ -27,9 +26,9 @@ class PhoneAuthHelper {
|
||||
link: _sendOtpUrl,
|
||||
payload: {'receiver': phoneNumber},
|
||||
);
|
||||
Log.print('response: ${response}');
|
||||
if (response != 'failure') {
|
||||
final data = (response);
|
||||
Log.print('data: ${data}');
|
||||
// if (data['status'] == 'success') {
|
||||
mySnackbarSuccess('An OTP has been sent to your WhatsApp number.'.tr);
|
||||
return true;
|
||||
@@ -42,7 +41,6 @@ class PhoneAuthHelper {
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('e: ${e}');
|
||||
// mySnackeBarError('An error occurred: $e');
|
||||
return false;
|
||||
}
|
||||
@@ -61,17 +59,14 @@ class PhoneAuthHelper {
|
||||
|
||||
if (data['status'] == 'success') {
|
||||
final isRegistered = data['message']['isRegistered'] ?? false;
|
||||
Log.print('isRegistered: ${isRegistered}');
|
||||
box.write(BoxName.phoneVerified, true);
|
||||
box.write(BoxName.phoneDriver, phoneNumber);
|
||||
box.write(BoxName.driverID, data['message']['driverID']);
|
||||
Log.print('BoxName.driverID: ${box.read(BoxName.driverID)}');
|
||||
|
||||
if (isRegistered) {
|
||||
// ✅ السائق مسجل مسبقًا - سجل دخوله واذهب إلى الصفحة الرئيسية
|
||||
final driver = data['message']['driver'];
|
||||
// mySnackbarSuccess('Welcome back, ${driver['first_name']}!');
|
||||
Log.print('Welcome: }');
|
||||
|
||||
// حفظ بيانات السائق إذا أردت:
|
||||
box.write(BoxName.driverID, driver['id']);
|
||||
@@ -82,7 +77,8 @@ class PhoneAuthHelper {
|
||||
} else {
|
||||
// ✅ رقم الهاتف تم التحقق منه لكن السائق غير مسجل
|
||||
// mySnackbarSuccess('Phone verified. Please complete registration.');
|
||||
Get.to(() => SyrianCardAI());
|
||||
// Get.offAll(() => SyrianCardAI());
|
||||
Get.offAll(() => RegistrationView());
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError(data['message'] ?? 'Verification failed.');
|
||||
@@ -92,7 +88,6 @@ class PhoneAuthHelper {
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError('An error occurred: $e');
|
||||
Log.print('e: ${e}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +116,6 @@ class PhoneAuthHelper {
|
||||
"User with this phone number or email already exists.".tr);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('e: ${e}');
|
||||
mySnackeBarError('An error occurred: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
import 'package:sefer_driver/views/auth/captin/cards/syrian_card_a_i.dart';
|
||||
import 'package:sefer_driver/views/auth/captin/register_captin.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -17,9 +15,8 @@ import 'package:sefer_driver/views/auth/captin/verify_email_captain.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../views/auth/captin/ai_page.dart';
|
||||
import '../../../views/auth/captin/car_license_page.dart';
|
||||
import '../../../views/auth/syria/registration_view.dart';
|
||||
import '../../../views/home/Captin/home_captain/home_captin.dart';
|
||||
import '../../functions/encrypt_decrypt.dart';
|
||||
import '../../functions/sms_egypt_controller.dart';
|
||||
|
||||
class RegisterCaptainController extends GetxController {
|
||||
@@ -282,7 +279,8 @@ class RegisterCaptainController extends GetxController {
|
||||
// box.read(BoxName.driverID).toString(),
|
||||
// box.read(BoxName.emailDriver).toString(),
|
||||
// );
|
||||
Get.to(SyrianCardAI());
|
||||
// Get.offAll(() => SyrianCardAI());
|
||||
Get.offAll(() => RegistrationView());
|
||||
// } else {
|
||||
// Get.snackbar('title', 'message');
|
||||
// }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||
import 'package:sefer_driver/main.dart';
|
||||
import 'package:sefer_driver/views/auth/captin/cards/sms_signup.dart';
|
||||
import 'package:sefer_driver/views/home/on_boarding_page.dart';
|
||||
@@ -68,9 +69,10 @@ class GoogleSignInHelper {
|
||||
}
|
||||
|
||||
return googleUser;
|
||||
} catch (error) {
|
||||
} catch (error, stackTrace) {
|
||||
mySnackeBarError('$error');
|
||||
addError(error.toString(), 'GoogleSignInAccount?> signInFromLogin()');
|
||||
CRUD.addError(error.toString(), stackTrace.toString(),
|
||||
'GoogleSignInAccount?> signInFromLogin()');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
import 'package:sefer_driver/constant/links.dart';
|
||||
import '../../../constant/box_name.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
// --- Final Submission ---
|
||||
@@ -19,8 +20,10 @@ import 'package:path/path.dart' as p;
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/encrypt_decrypt.dart';
|
||||
import '../captin/login_captin_controller.dart';
|
||||
|
||||
// You can create a simple enum to manage image types
|
||||
enum ImageType {
|
||||
@@ -222,6 +225,97 @@ class RegistrationController extends GetxController {
|
||||
return Color(int.parse(v, radix: 16));
|
||||
}
|
||||
|
||||
//uploadSyrianDocs
|
||||
// دالة مساعدة: تضيف الحقل إذا كان له قيمة
|
||||
void _addField(Map<String, String> fields, String key, String? value) {
|
||||
if (value != null && value.toString().isNotEmpty) {
|
||||
fields[key] = value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// دالة رفع إلى السيرفر السوري: ترجع file_url (Signed URL)
|
||||
Future<String> uploadToSyria({
|
||||
required String docType,
|
||||
required File file,
|
||||
required Uri syrianUploadUri,
|
||||
required String authHeader,
|
||||
required String hmacHeader,
|
||||
required String driverId,
|
||||
Duration timeout = const Duration(seconds: 60),
|
||||
http.Client? clientOverride,
|
||||
}) async {
|
||||
final client = clientOverride ?? http.Client();
|
||||
try {
|
||||
final mime = lookupMimeType(file.path) ?? 'image/jpeg';
|
||||
final parts = mime.split('/');
|
||||
|
||||
final req = http.MultipartRequest('POST', syrianUploadUri);
|
||||
req.headers.addAll({
|
||||
'Authorization': authHeader,
|
||||
'X-HMAC-Auth': hmacHeader,
|
||||
});
|
||||
|
||||
req.fields['driver_id'] = driverId;
|
||||
req.fields['doc_type'] = docType;
|
||||
|
||||
req.files.add(
|
||||
await http.MultipartFile.fromPath(
|
||||
'file',
|
||||
file.path,
|
||||
filename: p.basename(file.path),
|
||||
contentType: MediaType(parts.first, parts.last),
|
||||
),
|
||||
);
|
||||
|
||||
// ====== الطباعة قبل الإرسال ======
|
||||
// Log.print('--- Syrian Upload Request ---');
|
||||
// Log.print('URL: $syrianUploadUri');
|
||||
// // Log.print('Method: POST');
|
||||
// // Log.print('Headers: ${req.headers}');
|
||||
// Log.print('Fields: ${req.fields}');
|
||||
// // Log.print(
|
||||
// // 'File: ${file.path} (${await file.length()} bytes, mime: $mime)');
|
||||
// Log.print('-----------------------------');
|
||||
|
||||
// الإرسال
|
||||
final streamed = await client.send(req).timeout(timeout);
|
||||
final resp = await http.Response.fromStream(streamed);
|
||||
|
||||
// ====== الطباعة بعد الاستجابة ======
|
||||
// Log.print('--- Syrian Upload Response ---');
|
||||
Log.print('Status: ${resp.statusCode}');
|
||||
// Log.print('Headers: ${resp.headers}');
|
||||
// Log.print('Body: ${resp.body}');
|
||||
// Log.print('-------------------------------');
|
||||
|
||||
Map<String, dynamic> j = {};
|
||||
try {
|
||||
j = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
Log.print('⚠️ Failed to parse JSON: $e');
|
||||
}
|
||||
|
||||
// التحمّل لشكلين من الـ JSON:
|
||||
final statusOk = j['status'] == 'success';
|
||||
final fileUrl = (j['file_url'] ?? j['message']?['file_url'])?.toString();
|
||||
final fileName =
|
||||
(j['file_name'] ?? j['message']?['file_name'])?.toString();
|
||||
|
||||
if (resp.statusCode == 200 &&
|
||||
statusOk &&
|
||||
(fileUrl?.isNotEmpty ?? false)) {
|
||||
// Log.print(
|
||||
// '✅ Syrian upload success: $fileUrl (file: ${fileName ?? "-"})');
|
||||
return fileUrl!;
|
||||
}
|
||||
|
||||
throw Exception(
|
||||
'❌ Syrian upload failed ($docType): ${j['message'] ?? resp.body}');
|
||||
} finally {
|
||||
if (clientOverride == null) client.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> submitRegistration() async {
|
||||
// 1) تحقق من الصور
|
||||
if (driverLicenseFrontImage == null ||
|
||||
@@ -229,30 +323,82 @@ class RegistrationController extends GetxController {
|
||||
carLicenseFrontImage == null ||
|
||||
carLicenseBackImage == null) {
|
||||
Get.snackbar(
|
||||
'Missing Documents'.tr, 'Please upload all 4 required documents.'.tr,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white);
|
||||
'Missing Documents'.tr,
|
||||
'Please upload all 4 required documents.'.tr,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
final uri = Uri.parse(
|
||||
'https://intaleq.xyz/intaleq/auth/syria/driver/register_driver_and_car.php',
|
||||
);
|
||||
// روابط الـ API
|
||||
final registerUri =
|
||||
Uri.parse(AppLink.register_driver_and_car); // التسجيل الرئيسي (PHP)
|
||||
final syrianUploadUri =
|
||||
// Uri.parse(AppLink.uploadSyrianDocs); // رفع الصور في سوريا
|
||||
Uri.parse(
|
||||
'https://syria.intaleq.xyz/intaleq/auth/syria/uploadSyrianDocs.php'); // رفع الصور في سوريا
|
||||
|
||||
final client = http.Client();
|
||||
try {
|
||||
final req = http.MultipartRequest('POST', uri);
|
||||
// ترويسات مشتركة
|
||||
final bearer =
|
||||
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}';
|
||||
final hmac = '${box.read(BoxName.hmac)}';
|
||||
|
||||
// 2) ارفع الصور أولاً على السيرفر السوري واحصل على روابطها (Signed URLs)
|
||||
final driverId = (box.read(BoxName.driverID) ?? '').toString();
|
||||
|
||||
final driverFrontUrl = await uploadToSyria(
|
||||
docType: 'driver_license_front',
|
||||
file: driverLicenseFrontImage!,
|
||||
syrianUploadUri: syrianUploadUri,
|
||||
authHeader: bearer,
|
||||
hmacHeader: hmac,
|
||||
driverId: driverId,
|
||||
clientOverride: client,
|
||||
);
|
||||
|
||||
final driverBackUrl = await uploadToSyria(
|
||||
docType: 'driver_license_back',
|
||||
file: driverLicenseBackImage!,
|
||||
syrianUploadUri: syrianUploadUri,
|
||||
authHeader: bearer,
|
||||
hmacHeader: hmac,
|
||||
driverId: driverId,
|
||||
clientOverride: client,
|
||||
);
|
||||
|
||||
final carFrontUrl = await uploadToSyria(
|
||||
docType: 'car_license_front',
|
||||
file: carLicenseFrontImage!,
|
||||
syrianUploadUri: syrianUploadUri,
|
||||
authHeader: bearer,
|
||||
hmacHeader: hmac,
|
||||
driverId: driverId,
|
||||
clientOverride: client,
|
||||
);
|
||||
|
||||
final carBackUrl = await uploadToSyria(
|
||||
docType: 'car_license_back',
|
||||
file: carLicenseBackImage!,
|
||||
syrianUploadUri: syrianUploadUri,
|
||||
authHeader: bearer,
|
||||
hmacHeader: hmac,
|
||||
driverId: driverId,
|
||||
clientOverride: client,
|
||||
);
|
||||
|
||||
// 3) جهّز طلب التسجيل الرئيسي: نرسل الحقول + روابط الصور (لا نرفع الصور مرة ثانية)
|
||||
final req = http.MultipartRequest('POST', registerUri);
|
||||
req.headers.addAll({
|
||||
'Authorization': bearer,
|
||||
'X-HMAC-Auth': hmac,
|
||||
});
|
||||
|
||||
// مهم: لا تضع Content-Type يدويًا، الـ MultipartRequest يتكفّل فيه ببناء boundary.
|
||||
final headers = {
|
||||
'Authorization':
|
||||
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}',
|
||||
'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
||||
};
|
||||
// 2) الحقول النصية
|
||||
final fields = <String, String>{};
|
||||
|
||||
// --- Driver Data ---
|
||||
@@ -266,48 +412,33 @@ class RegistrationController extends GetxController {
|
||||
fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك
|
||||
_addField(fields, 'status', 'yet');
|
||||
_addField(fields, 'email',
|
||||
'Not specified'); // سكربت السيرفر سيحوّلها null ويبني ايميل افتراضي
|
||||
'Not specified'); // السيرفر سيحوّلها null ويبني ايميل افتراضي
|
||||
_addField(fields, 'gender', 'Male');
|
||||
|
||||
// --- Car Data (مطابقة لما يتوقّعه السكربت) ---
|
||||
_addField(fields, 'vin', 'carVinController.text);');
|
||||
// --- Car Data ---
|
||||
_addField(fields, 'vin', 'yet'); // تم تصحيح الاقتباس
|
||||
_addField(fields, 'car_plate', carPlateController.text);
|
||||
_addField(fields, 'make', carMakeController.text);
|
||||
_addField(fields, 'model', carModelController.text);
|
||||
_addField(fields, 'year', carYearController.text);
|
||||
_addField(fields, 'expiration_date', 'carRegistrationExpiryController');
|
||||
_addField(fields, 'expiration_date',
|
||||
driverLicenseExpiryController.text); // تم التصحيح
|
||||
_addField(fields, 'color', carColorController.text);
|
||||
_addField(fields, 'fuel', 'Gasoline'); // أو حسب اختيارك
|
||||
_addField(fields, 'color_hex', colorHex); // مهم
|
||||
// لو عندك حقول إضافية مطلوبة بالسكربت (مالك المركبة / الكود اللوني / الوقود) مرّرها:
|
||||
_addField(fields, 'owner',
|
||||
firstNameController.text + ' ' + lastNameController.text);
|
||||
// if (colorHex != null) _addField(fields, 'color_hex', colorHex);
|
||||
// if (fuelType != null) _addField(fields, 'fuel', fuelType);
|
||||
req.headers.addAll(headers);
|
||||
req.fields.addAll(fields);
|
||||
|
||||
// 3) الملفات (4 صور) — مفاتيحها مطابقة للسكربت
|
||||
Future<void> addFile(String field, File file) async {
|
||||
final mime = lookupMimeType(file.path) ?? 'image/jpeg';
|
||||
final parts = mime.split('/');
|
||||
final mediaType = MediaType(parts.first, parts.last);
|
||||
req.files.add(
|
||||
await http.MultipartFile.fromPath(
|
||||
field,
|
||||
file.path,
|
||||
filename: p.basename(file.path),
|
||||
contentType: mediaType,
|
||||
),
|
||||
);
|
||||
_addField(fields, 'fuel', 'Gasoline');
|
||||
if (colorHex != null && colorHex!.isNotEmpty) {
|
||||
_addField(fields, 'color_hex', colorHex!);
|
||||
}
|
||||
_addField(fields, 'owner',
|
||||
'${firstNameController.text} ${lastNameController.text}');
|
||||
|
||||
await addFile('driver_license_front', driverLicenseFrontImage!);
|
||||
await addFile('driver_license_back', driverLicenseBackImage!);
|
||||
await addFile('car_license_front', carLicenseFrontImage!);
|
||||
await addFile('car_license_back', carLicenseBackImage!);
|
||||
// --- روابط الصور الموقّعة من سوريا ---
|
||||
_addField(fields, 'driver_license_front', driverFrontUrl);
|
||||
_addField(fields, 'driver_license_back', driverBackUrl);
|
||||
_addField(fields, 'car_license_front', carFrontUrl);
|
||||
_addField(fields, 'car_license_back', carBackUrl);
|
||||
|
||||
// (اختياري) هيدر للقبول بـ JSON
|
||||
// أضف الحقول
|
||||
req.fields.addAll(fields);
|
||||
|
||||
// 4) الإرسال
|
||||
final streamed =
|
||||
@@ -320,46 +451,187 @@ class RegistrationController extends GetxController {
|
||||
json = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||
} catch (_) {}
|
||||
|
||||
if (resp.statusCode == 200 &&
|
||||
json != null &&
|
||||
json['status'] == 'success') {
|
||||
// ممكن يرجّع driverID, carRegID, documents
|
||||
if (resp.statusCode == 200 && json?['status'] == 'success') {
|
||||
final driverID =
|
||||
(json['data']?['driverID'] ?? json['driverID'])?.toString();
|
||||
(json!['data']?['driverID'] ?? json['driverID'])?.toString();
|
||||
if (driverID != null && driverID.isNotEmpty) {
|
||||
box.write(BoxName.driverID, driverID);
|
||||
}
|
||||
|
||||
Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white);
|
||||
Get.snackbar(
|
||||
'Success'.tr,
|
||||
'Registration completed successfully!'.tr,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
// TODO: انتقل للصفحة التالية أو حدّث الحالة…
|
||||
// TODO: التنقّل أو تحديث الحالة…
|
||||
final email = box.read<String?>(BoxName.emailDriver) ?? '';
|
||||
|
||||
final c = Get.isRegistered<LoginDriverController>()
|
||||
? Get.find<LoginDriverController>()
|
||||
: Get.put(LoginDriverController());
|
||||
|
||||
c.loginWithGoogleCredential(driverId, email);
|
||||
} else {
|
||||
final msg =
|
||||
(json?['message'] ?? 'Registration failed. Please try again.')
|
||||
.toString();
|
||||
Get.snackbar('Error'.tr, msg,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('Error'.tr, '${'An unexpected error occurred:'.tr} $e',
|
||||
Log.print('msg: ${msg}');
|
||||
|
||||
Get.snackbar(
|
||||
'Error'.tr,
|
||||
msg,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white);
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error'.tr,
|
||||
'${'An unexpected error occurred:'.tr} $e',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} finally {
|
||||
client.close();
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
// Future<void> submitRegistration() async {
|
||||
// // 1) تحقق من الصور
|
||||
// if (driverLicenseFrontImage == null ||
|
||||
// driverLicenseBackImage == null ||
|
||||
// carLicenseFrontImage == null ||
|
||||
// carLicenseBackImage == null) {
|
||||
// Get.snackbar(
|
||||
// 'Missing Documents'.tr, 'Please upload all 4 required documents.'.tr,
|
||||
// snackPosition: SnackPosition.BOTTOM,
|
||||
// backgroundColor: Colors.orange,
|
||||
// colorText: Colors.white);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// isLoading.value = true;
|
||||
|
||||
// final uri = Uri.parse(
|
||||
// 'https://intaleq.xyz/intaleq/auth/syria/driver/register_driver_and_car.php',
|
||||
// );
|
||||
|
||||
// final client = http.Client();
|
||||
// try {
|
||||
// final req = http.MultipartRequest('POST', uri);
|
||||
|
||||
// // مهم: لا تضع Content-Type يدويًا، الـ MultipartRequest يتكفّل فيه ببناء boundary.
|
||||
// final headers = {
|
||||
// 'Authorization':
|
||||
// 'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}',
|
||||
// 'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
||||
// };
|
||||
// // 2) الحقول النصية
|
||||
// final fields = <String, String>{};
|
||||
|
||||
// // --- Driver Data ---
|
||||
// _addField(fields, 'id', box.read(BoxName.driverID)?.toString());
|
||||
// _addField(fields, 'first_name', firstNameController.text);
|
||||
// _addField(fields, 'last_name', lastNameController.text);
|
||||
// _addField(fields, 'phone', box.read(BoxName.phoneDriver) ?? '');
|
||||
// _addField(fields, 'national_number', nationalIdController.text);
|
||||
// _addField(fields, 'expiry_date', driverLicenseExpiryController.text);
|
||||
// _addField(
|
||||
// fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك
|
||||
// _addField(fields, 'status', 'yet');
|
||||
// _addField(fields, 'email',
|
||||
// 'Not specified'); // سكربت السيرفر سيحوّلها null ويبني ايميل افتراضي
|
||||
// _addField(fields, 'gender', 'Male');
|
||||
|
||||
// // --- Car Data (مطابقة لما يتوقّعه السكربت) ---
|
||||
// _addField(fields, 'vin', 'carVinController.text);');
|
||||
// _addField(fields, 'car_plate', carPlateController.text);
|
||||
// _addField(fields, 'make', carMakeController.text);
|
||||
// _addField(fields, 'model', carModelController.text);
|
||||
// _addField(fields, 'year', carYearController.text);
|
||||
// _addField(fields, 'expiration_date', 'carRegistrationExpiryController');
|
||||
// _addField(fields, 'color', carColorController.text);
|
||||
// _addField(fields, 'fuel', 'Gasoline'); // أو حسب اختيارك
|
||||
// _addField(fields, 'color_hex', colorHex); // مهم
|
||||
// // لو عندك حقول إضافية مطلوبة بالسكربت (مالك المركبة / الكود اللوني / الوقود) مرّرها:
|
||||
// _addField(fields, 'owner',
|
||||
// firstNameController.text + ' ' + lastNameController.text);
|
||||
// // if (colorHex != null) _addField(fields, 'color_hex', colorHex);
|
||||
// // if (fuelType != null) _addField(fields, 'fuel', fuelType);
|
||||
// req.headers.addAll(headers);
|
||||
// req.fields.addAll(fields);
|
||||
|
||||
// // 3) الملفات (4 صور) — مفاتيحها مطابقة للسكربت
|
||||
// Future<void> addFile(String field, File file) async {
|
||||
// final mime = lookupMimeType(file.path) ?? 'image/jpeg';
|
||||
// final parts = mime.split('/');
|
||||
// final mediaType = MediaType(parts.first, parts.last);
|
||||
// req.files.add(
|
||||
// await http.MultipartFile.fromPath(
|
||||
// field,
|
||||
// file.path,
|
||||
// filename: p.basename(file.path),
|
||||
// contentType: mediaType,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// await addFile('driver_license_front', driverLicenseFrontImage!);
|
||||
// await addFile('driver_license_back', driverLicenseBackImage!);
|
||||
// await addFile('car_license_front', carLicenseFrontImage!);
|
||||
// await addFile('car_license_back', carLicenseBackImage!);
|
||||
|
||||
// // 4) الإرسال
|
||||
// final streamed =
|
||||
// await client.send(req).timeout(const Duration(seconds: 60));
|
||||
// final resp = await http.Response.fromStream(streamed);
|
||||
|
||||
// // 5) فحص النتيجة
|
||||
// Map<String, dynamic>? json;
|
||||
// try {
|
||||
// json = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||
// } catch (_) {}
|
||||
|
||||
// if (resp.statusCode == 200 &&
|
||||
// json != null &&
|
||||
// json['status'] == 'success') {
|
||||
// // ممكن يرجّع driverID, carRegID, documents
|
||||
// final driverID =
|
||||
// (json['data']?['driverID'] ?? json['driverID'])?.toString();
|
||||
// if (driverID != null && driverID.isNotEmpty) {
|
||||
// box.write(BoxName.driverID, driverID);
|
||||
// }
|
||||
|
||||
// Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr,
|
||||
// snackPosition: SnackPosition.BOTTOM,
|
||||
// backgroundColor: Colors.green,
|
||||
// colorText: Colors.white);
|
||||
|
||||
// // TODO: انتقل للصفحة التالية أو حدّث الحالة…
|
||||
// } else {
|
||||
// final msg =
|
||||
// (json?['message'] ?? 'Registration failed. Please try again.')
|
||||
// .toString();
|
||||
// Get.snackbar('Error'.tr, msg,
|
||||
// snackPosition: SnackPosition.BOTTOM,
|
||||
// backgroundColor: Colors.red,
|
||||
// colorText: Colors.white);
|
||||
// }
|
||||
// } catch (e) {
|
||||
// Get.snackbar('Error'.tr, '${'An unexpected error occurred:'.tr} $e',
|
||||
// snackPosition: SnackPosition.BOTTOM,
|
||||
// backgroundColor: Colors.red,
|
||||
// colorText: Colors.white);
|
||||
// } finally {
|
||||
// client.close();
|
||||
// isLoading.value = false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Helpers
|
||||
void _addField(Map<String, String> fields, String key, String? value) {
|
||||
if (value != null && value.toString().trim().isNotEmpty) {
|
||||
fields[key] = value.toString().trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,6 +344,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () {
|
||||
box.write(BoxName.rideStatus, 'Cancel');
|
||||
box.write(BoxName.statusDriverLocation, 'off');
|
||||
Log.print(
|
||||
'rideStatus from 347 : ${box.read(BoxName.rideStatus)}');
|
||||
Get.offAll(HomeCaptain());
|
||||
|
||||
@@ -3,17 +3,30 @@ import '../../constant/links.dart';
|
||||
import '../../main.dart';
|
||||
import 'crud.dart';
|
||||
|
||||
addError(String error, where) async {
|
||||
CRUD().post(link: AppLink.addError, payload: {
|
||||
'error': error.toString(), // Example error description
|
||||
'userId': box.read(BoxName.driverID) ??
|
||||
box.read(BoxName.passengerID), // Example user ID
|
||||
'userType': box.read(BoxName.driverID) != null
|
||||
? 'Driver'
|
||||
: 'passenger', // Example user type
|
||||
'phone': box.read(BoxName.phone) ??
|
||||
box.read(BoxName.phoneDriver), // Example phone number
|
||||
addError1(String error, String details, String where) async {
|
||||
try {
|
||||
// Get user information for the error log
|
||||
final userId = box.read(BoxName.driverID) ?? box.read(BoxName.passengerID);
|
||||
final userType =
|
||||
box.read(BoxName.driverID) != null ? 'Driver' : 'passenger';
|
||||
final phone = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver);
|
||||
|
||||
'device': where
|
||||
});
|
||||
// Send the error data to the server
|
||||
// Note: This is a fire-and-forget call. We don't await it or handle its response
|
||||
// to prevent an infinite loop if the addError endpoint itself is failing.
|
||||
CRUD().post(
|
||||
link: AppLink.addError,
|
||||
payload: {
|
||||
'error': error.toString(),
|
||||
'userId': userId.toString(),
|
||||
'userType': userType,
|
||||
'phone': phone.toString(),
|
||||
'device': where, // The location of the error
|
||||
'details': details, // The detailed stack trace or context
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// If logging the error itself fails, print to the console to avoid infinite loops.
|
||||
print("Failed to log error to server: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,201 @@ import 'gemeni.dart';
|
||||
import 'upload_image.dart';
|
||||
|
||||
class CRUD {
|
||||
/// Stores the signature of the last logged error to prevent duplicates.
|
||||
static String _lastErrorSignature = '';
|
||||
|
||||
/// Stores the timestamp of the last logged error.
|
||||
static DateTime _lastErrorTimestamp =
|
||||
DateTime(2000); // Initialize with an old date
|
||||
/// The minimum time that must pass before logging the same error again.
|
||||
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
|
||||
|
||||
/// Asynchronously logs an error to the server with debouncing to prevent log flooding.
|
||||
///
|
||||
/// [error]: A concise description of the error.
|
||||
/// [details]: Detailed information, such as a stack trace or the server response body.
|
||||
/// [where]: The location in the code where the error occurred (e.g., 'ClassName.methodName').
|
||||
static Future<void> addError(
|
||||
String error, String details, String where) async {
|
||||
try {
|
||||
// Create a unique signature for the current error
|
||||
final currentErrorSignature = '$where-$error';
|
||||
final now = DateTime.now();
|
||||
|
||||
// Check if the same error occurred recently
|
||||
if (currentErrorSignature == _lastErrorSignature &&
|
||||
now.difference(_lastErrorTimestamp) < _errorLogDebounceDuration) {
|
||||
// If it's the same error within the debounce duration, ignore it.
|
||||
print("Debounced a duplicate error: $error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the signature and timestamp for the new error
|
||||
_lastErrorSignature = currentErrorSignature;
|
||||
_lastErrorTimestamp = now;
|
||||
|
||||
// Get user information for the error log
|
||||
final userId =
|
||||
box.read(BoxName.driverID) ?? box.read(BoxName.passengerID);
|
||||
final userType =
|
||||
box.read(BoxName.driverID) != null ? 'Driver' : 'passenger';
|
||||
final phone = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver);
|
||||
|
||||
// Send the error data to the server
|
||||
// Note: This is a fire-and-forget call. We don't await it or handle its response
|
||||
// to prevent an infinite loop if the addError endpoint itself is failing.
|
||||
CRUD().post(
|
||||
link: AppLink.addError,
|
||||
payload: {
|
||||
'error': error.toString(),
|
||||
'userId': userId.toString(),
|
||||
'userType': userType,
|
||||
'phone': phone.toString(),
|
||||
'device': where, // The location of the error
|
||||
'details': details, // The detailed stack trace or context
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// If logging the error itself fails, print to the console to avoid infinite loops.
|
||||
print("Failed to log error to server: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> _makeRequest({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
required Map<String, String> headers,
|
||||
}) async {
|
||||
var url = Uri.parse(link);
|
||||
try {
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
// Handle successful response (200 OK)
|
||||
if (response.statusCode == 200) {
|
||||
try {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['status'] == 'success') {
|
||||
return jsonData; // Return the full JSON object on success
|
||||
} else {
|
||||
// The API reported a logical failure (e.g., validation error)
|
||||
addError(
|
||||
'API Logic Error: ${jsonData['status']}',
|
||||
'Response: ${response.body}',
|
||||
'CRUD._makeRequest - $link',
|
||||
);
|
||||
return jsonData['status']; // Return the specific status string
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
// Error decoding the JSON response from the server
|
||||
addError(
|
||||
'JSON Decode Error: $e',
|
||||
'Response Body: ${response.body}\nStack Trace: $stackTrace',
|
||||
'CRUD._makeRequest - $link',
|
||||
);
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
// Handle Unauthorized (401) - typically means token expired
|
||||
else if (response.statusCode == 401) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['error'] == 'Token expired') {
|
||||
// The token refresh logic is handled before the call,
|
||||
// but we log this case if it still happens.
|
||||
// addError(
|
||||
// 'Token Expired',
|
||||
// 'A new token should have been fetched before this call.',
|
||||
// 'CRUD._makeRequest - $link',
|
||||
// );
|
||||
return 'token_expired';
|
||||
} else {
|
||||
// Other 401 errors (e.g., invalid token)
|
||||
addError(
|
||||
'Unauthorized Error: ${jsonData['error']}',
|
||||
'Status Code: 401',
|
||||
'CRUD._makeRequest - $link',
|
||||
);
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
// Handle all other non-successful status codes
|
||||
else {
|
||||
addError(
|
||||
'HTTP Error',
|
||||
'Status Code: ${response.statusCode}\nResponse Body: ${response.body}',
|
||||
'CRUD._makeRequest - $link',
|
||||
);
|
||||
return 'failure';
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
// Handle network exceptions (e.g., no internet, DNS error)
|
||||
addError(
|
||||
'HTTP Request Exception: $e',
|
||||
'Stack Trace: $stackTrace',
|
||||
'CRUD._makeRequest - $link',
|
||||
);
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> post({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
// 1. Check if the token is expired
|
||||
bool isTokenExpired = JwtDecoder.isExpired(X
|
||||
.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
|
||||
.toString()
|
||||
.split(AppInformation.addd)[0]);
|
||||
|
||||
// 2. If expired, get a new one
|
||||
if (isTokenExpired) {
|
||||
await LoginDriverController().getJWT();
|
||||
}
|
||||
|
||||
// 3. Prepare the headers with the valid token
|
||||
final headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
'Authorization':
|
||||
'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}'
|
||||
};
|
||||
|
||||
// 4. Make the request using the centralized helper
|
||||
return await _makeRequest(
|
||||
link: link,
|
||||
payload: payload,
|
||||
headers: headers,
|
||||
);
|
||||
}
|
||||
|
||||
/// Performs an authenticated POST request to the wallet endpoints.
|
||||
/// Uses a separate JWT and HMAC for authentication.
|
||||
Future<dynamic> postWallet({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
// 1. Get the specific JWT and HMAC for the wallet
|
||||
var jwt = await LoginDriverController().getJwtWallet();
|
||||
final hmac = box.read(BoxName.hmac);
|
||||
|
||||
// 2. Prepare the headers
|
||||
final headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
'Authorization': 'Bearer $jwt',
|
||||
'X-HMAC-Auth': hmac.toString(),
|
||||
};
|
||||
|
||||
// 3. Make the request using the centralized helper
|
||||
return await _makeRequest(
|
||||
link: link,
|
||||
payload: payload,
|
||||
headers: headers,
|
||||
);
|
||||
}
|
||||
|
||||
Future<dynamic> get({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
@@ -125,132 +320,132 @@ class CRUD {
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> postWallet(
|
||||
{required String link, Map<String, dynamic>? payload}) async {
|
||||
var s = await LoginDriverController().getJwtWallet();
|
||||
// Log.print('jwt: ${s}');
|
||||
final hmac = box.read(BoxName.hmac);
|
||||
// Log.print('hmac: ${hmac}');
|
||||
var url = Uri.parse(link);
|
||||
// Log.print('url: ${url}');
|
||||
try {
|
||||
// await LoginDriverController().getJWT();
|
||||
// Future<dynamic> postWallet(
|
||||
// {required String link, Map<String, dynamic>? payload}) async {
|
||||
// var s = await LoginDriverController().getJwtWallet();
|
||||
// // Log.print('jwt: ${s}');
|
||||
// final hmac = box.read(BoxName.hmac);
|
||||
// // Log.print('hmac: ${hmac}');
|
||||
// var url = Uri.parse(link);
|
||||
// // Log.print('url: ${url}');
|
||||
// try {
|
||||
// // await LoginDriverController().getJWT();
|
||||
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
'Authorization': 'Bearer $s',
|
||||
'X-HMAC-Auth': hmac.toString(),
|
||||
},
|
||||
);
|
||||
// Log.print('response.request:${response.request}');
|
||||
// Log.print('response.body: ${response.body}');
|
||||
// Log.print('payload:$payload');
|
||||
if (response.statusCode == 200) {
|
||||
try {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['status'] == 'success') {
|
||||
return jsonData;
|
||||
} else {
|
||||
return jsonData['status'];
|
||||
}
|
||||
} catch (e) {
|
||||
// addError(e.toString(), 'crud().post - JSON decoding');
|
||||
return 'failure';
|
||||
}
|
||||
} else if (response.statusCode == 401) {
|
||||
// Specifically handle 401 Unauthorized
|
||||
var jsonData = jsonDecode(response.body);
|
||||
// var response = await http.post(
|
||||
// url,
|
||||
// body: payload,
|
||||
// headers: {
|
||||
// "Content-Type": "application/x-www-form-urlencoded",
|
||||
// 'Authorization': 'Bearer $s',
|
||||
// 'X-HMAC-Auth': hmac.toString(),
|
||||
// },
|
||||
// );
|
||||
// // Log.print('response.request:${response.request}');
|
||||
// // Log.print('response.body: ${response.body}');
|
||||
// // Log.print('payload:$payload');
|
||||
// if (response.statusCode == 200) {
|
||||
// try {
|
||||
// var jsonData = jsonDecode(response.body);
|
||||
// if (jsonData['status'] == 'success') {
|
||||
// return jsonData;
|
||||
// } else {
|
||||
// return jsonData['status'];
|
||||
// }
|
||||
// } catch (e) {
|
||||
// // addError(e.toString(), 'crud().post - JSON decoding');
|
||||
// return 'failure';
|
||||
// }
|
||||
// } else if (response.statusCode == 401) {
|
||||
// // Specifically handle 401 Unauthorized
|
||||
// var jsonData = jsonDecode(response.body);
|
||||
|
||||
if (jsonData['error'] == 'Token expired') {
|
||||
return 'token_expired'; // Return a specific value for token expiration
|
||||
} else {
|
||||
// Other 401 errors
|
||||
// addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401');
|
||||
return 'failure';
|
||||
}
|
||||
} else {
|
||||
// addError('Non-200 response code: ${response.statusCode}',
|
||||
// 'crud().post - Other');
|
||||
return 'failure';
|
||||
}
|
||||
} catch (e) {
|
||||
// addError('HTTP request error: $e', 'crud().post - HTTP');
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
// if (jsonData['error'] == 'Token expired') {
|
||||
// return 'token_expired'; // Return a specific value for token expiration
|
||||
// } else {
|
||||
// // Other 401 errors
|
||||
// // addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401');
|
||||
// return 'failure';
|
||||
// }
|
||||
// } else {
|
||||
// // addError('Non-200 response code: ${response.statusCode}',
|
||||
// // 'crud().post - Other');
|
||||
// return 'failure';
|
||||
// }
|
||||
// } catch (e) {
|
||||
// // addError('HTTP request error: $e', 'crud().post - HTTP');
|
||||
// return 'failure';
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<dynamic> post(
|
||||
{required String link, Map<String, dynamic>? payload}) async {
|
||||
var url = Uri.parse(link);
|
||||
try {
|
||||
bool isTokenExpired = JwtDecoder.isExpired(X
|
||||
.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
|
||||
.toString()
|
||||
.split(AppInformation.addd)[0]);
|
||||
if (isTokenExpired) {
|
||||
await LoginDriverController().getJWT();
|
||||
}
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
'Authorization':
|
||||
'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}'
|
||||
// 'Authorization': 'Bearer ${box.read(BoxName.jwt)}'
|
||||
},
|
||||
);
|
||||
// print(response.request);
|
||||
// Log.print('response.body: ${response.body}');
|
||||
// print(payload);
|
||||
if (response.statusCode == 200) {
|
||||
try {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['status'] == 'success') {
|
||||
return jsonData;
|
||||
} else {
|
||||
return jsonData['status'];
|
||||
}
|
||||
} catch (e) {
|
||||
// addError(e.toString(), url);
|
||||
return 'failure';
|
||||
}
|
||||
} else if (response.statusCode == 401) {
|
||||
// Specifically handle 401 Unauthorized
|
||||
var jsonData = jsonDecode(response.body);
|
||||
// Future<dynamic> post(
|
||||
// {required String link, Map<String, dynamic>? payload}) async {
|
||||
// var url = Uri.parse(link);
|
||||
// try {
|
||||
// bool isTokenExpired = JwtDecoder.isExpired(X
|
||||
// .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
|
||||
// .toString()
|
||||
// .split(AppInformation.addd)[0]);
|
||||
// if (isTokenExpired) {
|
||||
// await LoginDriverController().getJWT();
|
||||
// }
|
||||
// var response = await http.post(
|
||||
// url,
|
||||
// body: payload,
|
||||
// headers: {
|
||||
// "Content-Type": "application/x-www-form-urlencoded",
|
||||
// 'Authorization':
|
||||
// 'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}'
|
||||
// // 'Authorization': 'Bearer ${box.read(BoxName.jwt)}'
|
||||
// },
|
||||
// );
|
||||
// print(response.request);
|
||||
// Log.print('response.body: ${response.body}');
|
||||
// print(payload);
|
||||
// if (response.statusCode == 200) {
|
||||
// try {
|
||||
// var jsonData = jsonDecode(response.body);
|
||||
// if (jsonData['status'] == 'success') {
|
||||
// return jsonData;
|
||||
// } else {
|
||||
// return jsonData['status'];
|
||||
// }
|
||||
// } catch (e) {
|
||||
// // addError(e.toString(), url);
|
||||
// return 'failure';
|
||||
// }
|
||||
// } else if (response.statusCode == 401) {
|
||||
// // Specifically handle 401 Unauthorized
|
||||
// var jsonData = jsonDecode(response.body);
|
||||
|
||||
if (jsonData['error'] == 'Token expired') {
|
||||
// Show snackbar prompting to re-login
|
||||
// await Get.put(LoginDriverController()).getJWT();
|
||||
// MyDialog().getDialog(
|
||||
// 'Session expired. Please log in again.'.tr,
|
||||
// '',
|
||||
// () {
|
||||
// Get.put(LoginController()).loginUsingCredentials(
|
||||
// box.read(BoxName.passengerID), box.read(BoxName.email));
|
||||
// Get.back();
|
||||
// },
|
||||
// );
|
||||
// if (jsonData['error'] == 'Token expired') {
|
||||
// // Show snackbar prompting to re-login
|
||||
// // await Get.put(LoginDriverController()).getJWT();
|
||||
// // MyDialog().getDialog(
|
||||
// // 'Session expired. Please log in again.'.tr,
|
||||
// // '',
|
||||
// // () {
|
||||
// // Get.put(LoginController()).loginUsingCredentials(
|
||||
// // box.read(BoxName.passengerID), box.read(BoxName.email));
|
||||
// // Get.back();
|
||||
// // },
|
||||
// // );
|
||||
|
||||
return 'token_expired'; // Return a specific value for token expiration
|
||||
} else {
|
||||
// Other 401 errors
|
||||
// addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401');
|
||||
return 'failure';
|
||||
}
|
||||
} else {
|
||||
// addError('Non-200 response code: ${response.statusCode}',
|
||||
// 'crud().post - Other');
|
||||
return 'failure';
|
||||
}
|
||||
} catch (e) {
|
||||
// addError('HTTP request error: $e', 'crud().post - HTTP');
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
// return 'token_expired'; // Return a specific value for token expiration
|
||||
// } else {
|
||||
// // Other 401 errors
|
||||
// // addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401');
|
||||
// return 'failure';
|
||||
// }
|
||||
// } else {
|
||||
// // addError('Non-200 response code: ${response.statusCode}',
|
||||
// // 'crud().post - Other');
|
||||
// return 'failure';
|
||||
// }
|
||||
// } catch (e) {
|
||||
// // addError('HTTP request error: $e', 'crud().post - HTTP');
|
||||
// return 'failure';
|
||||
// }
|
||||
// }
|
||||
|
||||
Future<dynamic> getAgoraToken({
|
||||
required String channelName,
|
||||
@@ -579,7 +774,10 @@ class CRUD {
|
||||
url,
|
||||
body: payload,
|
||||
);
|
||||
Log.print('esponse.body: ${response.body}');
|
||||
Log.print('esponse.body: ${response.request}');
|
||||
var jsonData = jsonDecode(response.body);
|
||||
|
||||
if (jsonData['status'] == 'OK') {
|
||||
return jsonData;
|
||||
}
|
||||
|
||||
@@ -290,7 +290,6 @@ class AI extends GetxController {
|
||||
'site': (idBackSy['address'].toString()) ?? 'Not specified',
|
||||
'employmentType': 'Not specified',
|
||||
};
|
||||
Log.print('payload driver: ${payload}');
|
||||
try {
|
||||
var res = await CRUD().post(link: AppLink.signUpCaptin, payload: payload);
|
||||
|
||||
@@ -303,7 +302,6 @@ class AI extends GetxController {
|
||||
isDriverSaved = true;
|
||||
box.write(BoxName.emailDriver,
|
||||
'${box.read(BoxName.phoneDriver)}${Env.email}');
|
||||
Log.print('BoxName.emailDriver: ${box.read(BoxName.emailDriver)}');
|
||||
mySnackbarSuccess('Driver data saved successfully');
|
||||
} else {
|
||||
mySnackeBarError('${'Failed to save driver data'.tr}: }');
|
||||
@@ -345,7 +343,6 @@ class AI extends GetxController {
|
||||
'color_hex': vehicleFrontSy['colorHex'].toString(),
|
||||
'fuel': vehicleBackSy['fuel'].toString(),
|
||||
};
|
||||
Log.print('payload: ${payload}');
|
||||
var res =
|
||||
await CRUD().post(link: AppLink.addRegisrationCar, payload: payload);
|
||||
isLoading = false;
|
||||
@@ -500,7 +497,6 @@ class AI extends GetxController {
|
||||
|
||||
final response = await request.send();
|
||||
final result = await http.Response.fromStream(response);
|
||||
Log.print('result: ${result.body}');
|
||||
|
||||
if (result.statusCode == 200) {
|
||||
final responseData = jsonDecode(result.body);
|
||||
@@ -557,7 +553,6 @@ class AI extends GetxController {
|
||||
isloading = false;
|
||||
update();
|
||||
MyDialog().getDialog("Error".tr, e.toString(), () => Get.back());
|
||||
Log.print('e: ${e}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,10 +615,7 @@ class AI extends GetxController {
|
||||
var extractedString =
|
||||
await CRUD().arabicTextExtractByVisionAndAI(imagePath: imagePath);
|
||||
var json = jsonDecode(extractedString);
|
||||
// Log.print('extractedString: ${extractedString}');
|
||||
var textValues = CRUD().extractTextFromLines(json);
|
||||
Log.print('textValues: $textValues');
|
||||
// Log.print('json: ${json}');
|
||||
|
||||
DocumentType detectedType = checkDocumentType(textValues);
|
||||
String expectedDocument = getExpectedDocument(imagePath);
|
||||
@@ -930,7 +922,6 @@ class AI extends GetxController {
|
||||
jsonDecode(responseData['content'][0]['text']);
|
||||
} else if (idType == 'non_id_front') {
|
||||
responseNonIdCardFront = jsonDecode(responseData['content'][0]['text']);
|
||||
Log.print('responseNonIdCardFront: $responseNonIdCardFront');
|
||||
} else if (idType == 'non_id_back') {
|
||||
responseNonIdCardBack = jsonDecode(responseData['content'][0]['text']);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ class HomeCaptainController extends GetxController {
|
||||
speedPrice = 0,
|
||||
deliveryPrice = 0,
|
||||
mashwariPrice = 0,
|
||||
familyPrice = 0,
|
||||
fuelPrice = 0;
|
||||
double naturePrice = 0;
|
||||
bool isCallOn = false;
|
||||
@@ -384,6 +385,7 @@ class HomeCaptainController extends GetxController {
|
||||
speedPrice = double.parse(json['message'][0]['speedPrice']);
|
||||
deliveryPrice = double.parse(json['message'][0]['deliveryPrice']);
|
||||
mashwariPrice = double.parse(json['message'][0]['freePrice']);
|
||||
familyPrice = double.parse(json['message'][0]['familyPrice']);
|
||||
fuelPrice = double.parse(json['message'][0]['fuelPrice']);
|
||||
}
|
||||
update();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
574
lib/controller/home/navigation/navigation_controller.dart
Normal file
574
lib/controller/home/navigation/navigation_controller.dart
Normal file
@@ -0,0 +1,574 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
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:sefer_driver/constant/colors.dart';
|
||||
|
||||
// استخدام نفس مسارات الاستيراد التي قدمتها
|
||||
import '../../../constant/api_key.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/tts.dart';
|
||||
|
||||
class NavigationController extends GetxController {
|
||||
// --- متغيرات الحالة العامة ---
|
||||
bool isLoading = false;
|
||||
GoogleMapController? mapController;
|
||||
final TextEditingController placeDestinationController =
|
||||
TextEditingController();
|
||||
|
||||
// --- متغيرات الخريطة والموقع ---
|
||||
LatLng? myLocation;
|
||||
double heading = 0.0;
|
||||
final Set<Marker> markers = {};
|
||||
final Set<Polyline> polylines = {};
|
||||
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
|
||||
BitmapDescriptor destinationIcon = BitmapDescriptor.defaultMarker;
|
||||
|
||||
// --- متغيرات النظام الذكي للتحديث ---
|
||||
Timer? _locationUpdateTimer; // المؤقت الرئيسي للتحكم في التحديثات
|
||||
Duration _currentUpdateInterval =
|
||||
const Duration(seconds: 2); // القيمة الافتراضية
|
||||
|
||||
// --- متغيرات البحث عن الأماكن ---
|
||||
List<dynamic> placesDestination = [];
|
||||
Timer? _debounce;
|
||||
|
||||
// --- متغيرات الملاحة (Navigation) ---
|
||||
LatLng? _finalDestination;
|
||||
List<Map<String, dynamic>> routeSteps = [];
|
||||
List<LatLng> _fullRouteCoordinates = [];
|
||||
List<List<LatLng>> _stepPolylines = []; // لتخزين نقاط كل خطوة على حدة
|
||||
bool _nextInstructionSpoken = false;
|
||||
|
||||
String currentInstruction = "";
|
||||
String nextInstruction = "";
|
||||
int currentStepIndex = 0;
|
||||
double currentSpeed = 0.0;
|
||||
String distanceToNextStep = "";
|
||||
final List<LatLngBounds> _stepBounds = [];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_initialize();
|
||||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
await _loadCustomIcons();
|
||||
await _getCurrentLocationAndStartUpdates();
|
||||
if (!Get.isRegistered<TextToSpeechController>()) {
|
||||
Get.put(TextToSpeechController());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_locationUpdateTimer?.cancel(); // إيقاف المؤقت عند إغلاق الصفحة
|
||||
mapController?.dispose();
|
||||
_debounce?.cancel();
|
||||
placeDestinationController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// ١. النظام الذكي لتحديد الموقع والتحديث
|
||||
// =======================================================================
|
||||
|
||||
Future<void> _getCurrentLocationAndStartUpdates() async {
|
||||
try {
|
||||
Position position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
myLocation = LatLng(position.latitude, position.longitude);
|
||||
update();
|
||||
animateCameraToPosition(myLocation!);
|
||||
// بدء التحديثات باستخدام المؤقت بدلاً من الـ Stream
|
||||
_startLocationTimer();
|
||||
} catch (e) {
|
||||
print("Error getting location: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// --- تم استبدال الـ Stream بمؤقت للتحكم الكامل ---
|
||||
void _startLocationTimer() {
|
||||
_locationUpdateTimer?.cancel(); // إلغاء أي مؤقت قديم
|
||||
_locationUpdateTimer = Timer.periodic(_currentUpdateInterval, (timer) {
|
||||
_updateLocationAndProcess();
|
||||
});
|
||||
}
|
||||
|
||||
// --- هذه الدالة هي التي تعمل الآن بشكل دوري ---
|
||||
Future<void> _updateLocationAndProcess() async {
|
||||
try {
|
||||
// طلب موقع واحد فقط عند كل مرة يعمل فيها المؤقت
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
myLocation = LatLng(position.latitude, position.longitude);
|
||||
heading = position.heading;
|
||||
currentSpeed = position.speed * 3.6;
|
||||
|
||||
_updateCarMarker();
|
||||
|
||||
if (polylines.isNotEmpty && myLocation != null) {
|
||||
animateCameraToPosition(
|
||||
myLocation!,
|
||||
bearing: heading,
|
||||
zoom: 18.5,
|
||||
);
|
||||
|
||||
_checkNavigationStep(myLocation!);
|
||||
}
|
||||
update();
|
||||
} catch (e) {
|
||||
print("Error in _updateLocationAndProcess: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// --- الدالة المسؤولة عن تغيير سرعة التحديث ديناميكياً ---
|
||||
void _adjustUpdateInterval() {
|
||||
if (currentStepIndex >= routeSteps.length) return;
|
||||
|
||||
final currentStepDistance =
|
||||
routeSteps[currentStepIndex]['distance']['value'];
|
||||
|
||||
// إذا كانت الخطوة الحالية طويلة (شارع سريع > 1.5 كم)
|
||||
if (currentStepDistance > 1500) {
|
||||
_currentUpdateInterval = const Duration(seconds: 4);
|
||||
}
|
||||
// إذا كانت الخطوة قصيرة (منعطفات داخل المدينة < 1.5 كم)
|
||||
else {
|
||||
_currentUpdateInterval = const Duration(seconds: 2);
|
||||
}
|
||||
|
||||
// إعادة تشغيل المؤقت بالسرعة الجديدة
|
||||
_startLocationTimer();
|
||||
}
|
||||
|
||||
// ... باقي دوال إعداد الخريطة ...
|
||||
void onMapCreated(GoogleMapController controller) {
|
||||
mapController = controller;
|
||||
if (myLocation != null) {
|
||||
animateCameraToPosition(myLocation!);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateCarMarker() {
|
||||
if (myLocation == null) return;
|
||||
markers.removeWhere((m) => m.markerId.value == 'myLocation');
|
||||
markers.add(
|
||||
Marker(
|
||||
markerId: const MarkerId('myLocation'),
|
||||
position: myLocation!,
|
||||
icon: carIcon,
|
||||
rotation: heading,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
flat: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void animateCameraToPosition(LatLng position,
|
||||
{double zoom = 16.0, double bearing = 0.0}) {
|
||||
mapController?.animateCamera(
|
||||
CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
target: position, zoom: zoom, bearing: bearing, tilt: 45.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// ٢. الملاحة والتحقق من الخطوات
|
||||
// =======================================================================
|
||||
|
||||
void _checkNavigationStep(LatLng currentPosition) {
|
||||
if (routeSteps.isEmpty ||
|
||||
currentStepIndex >= routeSteps.length ||
|
||||
_finalDestination == null) return;
|
||||
|
||||
_updateTraveledPolyline(currentPosition);
|
||||
|
||||
final step = routeSteps[currentStepIndex];
|
||||
final endLatLng =
|
||||
LatLng(step['end_location']['lat'], step['end_location']['lng']);
|
||||
final distance = Geolocator.distanceBetween(
|
||||
currentPosition.latitude,
|
||||
currentPosition.longitude,
|
||||
endLatLng.latitude,
|
||||
endLatLng.longitude,
|
||||
);
|
||||
|
||||
distanceToNextStep = (distance > 1000)
|
||||
? "${(distance / 1000).toStringAsFixed(1)} كم"
|
||||
: "${distance.toStringAsFixed(0)} متر";
|
||||
|
||||
if (distance < 30 &&
|
||||
!_nextInstructionSpoken &&
|
||||
nextInstruction.isNotEmpty) {
|
||||
Get.find<TextToSpeechController>().speakText(nextInstruction);
|
||||
_nextInstructionSpoken = true;
|
||||
}
|
||||
|
||||
if (distance < 20) {
|
||||
_advanceStep();
|
||||
}
|
||||
}
|
||||
|
||||
void _advanceStep() {
|
||||
currentStepIndex++;
|
||||
if (currentStepIndex < routeSteps.length) {
|
||||
currentInstruction =
|
||||
_parseInstruction(routeSteps[currentStepIndex]['html_instructions']);
|
||||
nextInstruction = ((currentStepIndex + 1) < routeSteps.length)
|
||||
? _parseInstruction(
|
||||
routeSteps[currentStepIndex + 1]['html_instructions'])
|
||||
: "الوجهة النهائية";
|
||||
_nextInstructionSpoken = false;
|
||||
|
||||
// **هنا يتم تعديل سرعة التحديث عند الانتقال لخطوة جديدة**
|
||||
_adjustUpdateInterval();
|
||||
|
||||
if (currentStepIndex < _stepBounds.length) {
|
||||
mapController?.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(_stepBounds[currentStepIndex], 70.0));
|
||||
}
|
||||
update();
|
||||
} else {
|
||||
currentInstruction = "لقد وصلت إلى وجهتك.";
|
||||
nextInstruction = "";
|
||||
distanceToNextStep = "";
|
||||
_locationUpdateTimer?.cancel(); // إيقاف التحديثات عند الوصول
|
||||
Get.find<TextToSpeechController>().speakText(currentInstruction);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// ٣. تحسين خوارزمية البحث ورسم المسار المقطوع
|
||||
// =======================================================================
|
||||
|
||||
void _updateTraveledPolyline(LatLng currentPosition) {
|
||||
// **التحسين:** البحث فقط في الخطوة الحالية والخطوة التالية
|
||||
int searchEndIndex = (currentStepIndex + 1 < _stepPolylines.length)
|
||||
? currentStepIndex + 1
|
||||
: currentStepIndex;
|
||||
|
||||
int overallClosestIndex = -1;
|
||||
double minDistance = double.infinity;
|
||||
|
||||
// البحث في نقاط الخطوة الحالية والتالية فقط
|
||||
for (int i = currentStepIndex; i <= searchEndIndex; i++) {
|
||||
for (int j = 0; j < _stepPolylines[i].length; j++) {
|
||||
final distance = Geolocator.distanceBetween(
|
||||
currentPosition.latitude,
|
||||
currentPosition.longitude,
|
||||
_stepPolylines[i][j].latitude,
|
||||
_stepPolylines[i][j].longitude);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
// نحتاج إلى حساب الفهرس العام في القائمة الكاملة
|
||||
overallClosestIndex = _getOverallIndex(i, j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (overallClosestIndex == -1) return;
|
||||
|
||||
List<LatLng> traveledPoints =
|
||||
_fullRouteCoordinates.sublist(0, overallClosestIndex + 1);
|
||||
traveledPoints.add(currentPosition);
|
||||
|
||||
List<LatLng> remainingPoints =
|
||||
_fullRouteCoordinates.sublist(overallClosestIndex);
|
||||
remainingPoints.insert(0, currentPosition);
|
||||
|
||||
polylines.removeWhere((p) => p.polylineId.value == 'traveled_route');
|
||||
polylines.add(Polyline(
|
||||
polylineId: const PolylineId('traveled_route'),
|
||||
points: traveledPoints,
|
||||
color: Colors.grey.shade600,
|
||||
width: 7,
|
||||
));
|
||||
|
||||
polylines.removeWhere((p) => p.polylineId.value == 'remaining_route');
|
||||
polylines.add(Polyline(
|
||||
polylineId: const PolylineId('remaining_route'),
|
||||
points: remainingPoints,
|
||||
color: const Color(0xFF4A80F0),
|
||||
width: 7,
|
||||
));
|
||||
}
|
||||
|
||||
// دالة مساعدة لحساب الفهرس العام
|
||||
int _getOverallIndex(int stepIndex, int pointInStepIndex) {
|
||||
int overallIndex = 0;
|
||||
for (int i = 0; i < stepIndex; i++) {
|
||||
overallIndex += _stepPolylines[i].length;
|
||||
}
|
||||
return overallIndex + pointInStepIndex;
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// ٤. دوال مساعدة وتجهيز البيانات
|
||||
// =======================================================================
|
||||
|
||||
void _prepareStepData() {
|
||||
_stepBounds.clear();
|
||||
_stepPolylines.clear();
|
||||
if (routeSteps.isEmpty) return;
|
||||
for (final step in routeSteps) {
|
||||
final pointsString = step['polyline']['points'];
|
||||
final List<List<num>> points =
|
||||
decodePolyline(pointsString).cast<List<num>>();
|
||||
final polylineCoordinates = points
|
||||
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
|
||||
.toList();
|
||||
_stepPolylines.add(polylineCoordinates); // تخزين نقاط الخطوة
|
||||
_stepBounds.add(_boundsFromLatLngList(polylineCoordinates));
|
||||
}
|
||||
}
|
||||
|
||||
// ... باقي دوال الكنترولر بدون تغيير ...
|
||||
// (selectDestination, onMapLongPressed, startNavigationTo, getRoute, etc.)
|
||||
Future<void> selectDestination(dynamic place) async {
|
||||
placeDestinationController.clear();
|
||||
placesDestination = [];
|
||||
|
||||
final double lat = double.parse(place['latitude'].toString());
|
||||
final double lng = double.parse(place['longitude'].toString());
|
||||
final LatLng destination = LatLng(lat, lng);
|
||||
|
||||
await startNavigationTo(destination,
|
||||
infoWindowTitle: place['name'] ?? 'وجهة محددة');
|
||||
}
|
||||
|
||||
Future<void> onMapLongPressed(LatLng tappedPoint) async {
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: const Text('بدء الملاحة؟'),
|
||||
content: const Text('هل تريد الذهاب إلى هذا الموقع المحدد؟'),
|
||||
actionsAlignment: MainAxisAlignment.spaceBetween,
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('إلغاء', style: TextStyle(color: Colors.grey)),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('اذهب الآن'),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
startNavigationTo(tappedPoint, infoWindowTitle: 'الموقع المحدد');
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> startNavigationTo(LatLng destination,
|
||||
{String infoWindowTitle = ''}) async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
_finalDestination = destination;
|
||||
clearRoute(isNewRoute: true);
|
||||
|
||||
markers.add(
|
||||
Marker(
|
||||
markerId: const MarkerId('destination'),
|
||||
position: destination,
|
||||
icon: destinationIcon,
|
||||
infoWindow: InfoWindow(title: infoWindowTitle),
|
||||
),
|
||||
);
|
||||
|
||||
await getRoute(myLocation!, destination);
|
||||
} catch (e) {
|
||||
Get.snackbar('خطأ', 'حدث خطأ أثناء تحديد الوجهة.');
|
||||
print("Error starting navigation: $e");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getRoute(LatLng origin, LatLng destination) async {
|
||||
final url =
|
||||
'${AppLink.googleMapsLink}directions/json?language=ar&destination=${destination.latitude},${destination.longitude}&origin=${origin.latitude},${origin.longitude}&key=${AK.mapAPIKEY}';
|
||||
var response = await CRUD().getGoogleApi(link: url, payload: {});
|
||||
|
||||
if (response == null || response['routes'].isEmpty) {
|
||||
Get.snackbar('خطأ', 'لم يتم العثور على مسار.');
|
||||
return;
|
||||
}
|
||||
|
||||
polylines.clear();
|
||||
final pointsString = response['routes'][0]['overview_polyline']['points'];
|
||||
final List<List<num>> points =
|
||||
decodePolyline(pointsString).cast<List<num>>();
|
||||
_fullRouteCoordinates = points
|
||||
.map((point) => LatLng(point[0].toDouble(), point[1].toDouble()))
|
||||
.toList();
|
||||
|
||||
polylines.add(
|
||||
Polyline(
|
||||
polylineId: const PolylineId('remaining_route'),
|
||||
points: _fullRouteCoordinates,
|
||||
color: const Color(0xFF4A80F0),
|
||||
width: 7,
|
||||
startCap: Cap.roundCap,
|
||||
endCap: Cap.roundCap,
|
||||
),
|
||||
);
|
||||
polylines.add(
|
||||
const Polyline(
|
||||
polylineId: PolylineId('traveled_route'),
|
||||
points: [],
|
||||
color: Colors.grey,
|
||||
width: 7,
|
||||
),
|
||||
);
|
||||
|
||||
routeSteps = List<Map<String, dynamic>>.from(
|
||||
response['routes'][0]['legs'][0]['steps']);
|
||||
_prepareStepData();
|
||||
|
||||
currentStepIndex = 0;
|
||||
_nextInstructionSpoken = false;
|
||||
if (routeSteps.isNotEmpty) {
|
||||
currentInstruction =
|
||||
_parseInstruction(routeSteps[0]['html_instructions']);
|
||||
nextInstruction = (routeSteps.length > 1)
|
||||
? _parseInstruction(routeSteps[1]['html_instructions'])
|
||||
: "الوجهة النهائية";
|
||||
Get.find<TextToSpeechController>().speakText(currentInstruction);
|
||||
}
|
||||
_adjustUpdateInterval(); // تحديد سرعة التحديث لأول مرة
|
||||
|
||||
final boundsData = response['routes'][0]['bounds'];
|
||||
mapController?.animateCamera(CameraUpdate.newLatLngBounds(
|
||||
LatLngBounds(
|
||||
northeast: LatLng(
|
||||
boundsData['northeast']['lat'], boundsData['northeast']['lng']),
|
||||
southwest: LatLng(
|
||||
boundsData['southwest']['lat'], boundsData['southwest']['lng']),
|
||||
),
|
||||
100.0,
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> recalculateRoute() async {
|
||||
if (myLocation == null || _finalDestination == null || isLoading) return;
|
||||
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
Get.snackbar(
|
||||
'إعادة التوجيه',
|
||||
'جاري حساب مسار جديد من موقعك الحالي...',
|
||||
backgroundColor: AppColor.goldenBronze,
|
||||
);
|
||||
await getRoute(myLocation!, _finalDestination!);
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
void clearRoute({bool isNewRoute = false}) {
|
||||
polylines.clear();
|
||||
if (!isNewRoute) {
|
||||
markers.removeWhere((m) => m.markerId.value == 'destination');
|
||||
_finalDestination = null;
|
||||
}
|
||||
routeSteps.clear();
|
||||
currentInstruction = "";
|
||||
nextInstruction = "";
|
||||
distanceToNextStep = "";
|
||||
currentSpeed = 0.0;
|
||||
_stepBounds.clear();
|
||||
_fullRouteCoordinates.clear();
|
||||
_stepPolylines.clear();
|
||||
_nextInstructionSpoken = false;
|
||||
_locationUpdateTimer?.cancel(); // إيقاف التحديثات عند إلغاء المسار
|
||||
update();
|
||||
}
|
||||
|
||||
LatLngBounds _boundsFromLatLngList(List<LatLng> list) {
|
||||
assert(list.isNotEmpty);
|
||||
double? x0, x1, y0, y1;
|
||||
for (LatLng latLng in list) {
|
||||
if (x0 == null) {
|
||||
x0 = x1 = latLng.latitude;
|
||||
y0 = y1 = latLng.longitude;
|
||||
} else {
|
||||
if (latLng.latitude > x1!) x1 = latLng.latitude;
|
||||
if (latLng.latitude < x0) x0 = latLng.latitude;
|
||||
if (latLng.longitude > y1!) y1 = latLng.longitude;
|
||||
if (latLng.longitude < y0!) y0 = latLng.longitude;
|
||||
}
|
||||
}
|
||||
return LatLngBounds(
|
||||
northeast: LatLng(x1!, y1!), southwest: LatLng(x0!, y0!));
|
||||
}
|
||||
|
||||
Future<void> _loadCustomIcons() async {
|
||||
carIcon = await BitmapDescriptor.fromAssetImage(
|
||||
const ImageConfiguration(size: Size(40, 40)), 'assets/images/car.png');
|
||||
destinationIcon = await BitmapDescriptor.fromAssetImage(
|
||||
const ImageConfiguration(size: Size(25, 25)), 'assets/images/b.png');
|
||||
}
|
||||
|
||||
String _parseInstruction(String html) =>
|
||||
html.replaceAll(RegExp(r'<[^>]*>'), ' ');
|
||||
|
||||
Future<void> getPlaces() async {
|
||||
if (placeDestinationController.text.trim().isEmpty) {
|
||||
placesDestination = [];
|
||||
update();
|
||||
return;
|
||||
}
|
||||
if (myLocation == null) {
|
||||
Get.snackbar('انتظر', 'جاري تحديد موقعك الحالي...');
|
||||
return;
|
||||
}
|
||||
final query = placeDestinationController.text.trim();
|
||||
final lat = myLocation!.latitude;
|
||||
final lng = myLocation!.longitude;
|
||||
const double range = 2.2;
|
||||
final lat_min = lat - range,
|
||||
lat_max = lat + range,
|
||||
lng_min = lng - range,
|
||||
lng_max = lng + range;
|
||||
|
||||
try {
|
||||
final response = await CRUD().post(
|
||||
link: AppLink.getPlacesSyria,
|
||||
payload: {
|
||||
'query': query,
|
||||
'lat_min': lat_min.toString(),
|
||||
'lat_max': lat_max.toString(),
|
||||
'lng_min': lng_min.toString(),
|
||||
'lng_max': lng_max.toString(),
|
||||
},
|
||||
);
|
||||
if (response != 'failure') {
|
||||
placesDestination = response['message'] ?? [];
|
||||
} else {
|
||||
placesDestination = [];
|
||||
}
|
||||
} catch (e) {
|
||||
print('Exception in getPlaces: $e');
|
||||
} finally {
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void onSearchChanged(String query) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 700), () => getPlaces());
|
||||
}
|
||||
}
|
||||
296
lib/controller/home/navigation/navigation_view.dart
Normal file
296
lib/controller/home/navigation/navigation_view.dart
Normal file
@@ -0,0 +1,296 @@
|
||||
// lib/views/navigation_view.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'navigation_controller.dart'; // For BackdropFilter
|
||||
|
||||
// استخدام نفس مسار الاستيراد الذي قدمته
|
||||
|
||||
class NavigationView extends StatelessWidget {
|
||||
const NavigationView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final NavigationController controller = Get.put(NavigationController());
|
||||
|
||||
return Scaffold(
|
||||
body: GetBuilder<NavigationController>(
|
||||
builder: (_) => Stack(
|
||||
children: [
|
||||
// --- الخريطة ---
|
||||
GoogleMap(
|
||||
onMapCreated: controller.onMapCreated,
|
||||
// --- السطر المضاف والمهم هنا ---
|
||||
onLongPress: controller.onMapLongPressed,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: controller.myLocation ??
|
||||
const LatLng(33.5138, 36.2765), // Default to Damascus
|
||||
zoom: 16.0,
|
||||
),
|
||||
markers: controller.markers,
|
||||
polylines: controller.polylines,
|
||||
myLocationEnabled: false,
|
||||
myLocationButtonEnabled: false,
|
||||
compassEnabled: false,
|
||||
zoomControlsEnabled: false,
|
||||
// تعديل الـ padding لإعطاء مساحة للعناصر العلوية والسفلية
|
||||
padding: EdgeInsets.only(
|
||||
bottom: controller.currentInstruction.isNotEmpty ? 130 : 0,
|
||||
top: 140),
|
||||
),
|
||||
|
||||
// --- واجهة البحث ونتائجه ---
|
||||
_buildSearchUI(controller),
|
||||
|
||||
// --- إرشادات الملاحة المطورة ---
|
||||
if (controller.currentInstruction.isNotEmpty)
|
||||
_buildNavigationInstruction(controller),
|
||||
|
||||
// --- أزرار التحكم على الخريطة ---
|
||||
_buildMapControls(controller),
|
||||
|
||||
// --- مؤشر التحميل ---
|
||||
if (controller.isLoading)
|
||||
Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- ويدجت خاصة بواجهة البحث ---
|
||||
Widget _buildSearchUI(NavigationController controller) {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller.placeDestinationController,
|
||||
onChanged: (val) {
|
||||
controller.onSearchChanged(val);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'إلى أين تريد الذهاب؟',
|
||||
prefixIcon: const Icon(Icons.search, color: Colors.grey),
|
||||
suffixIcon: controller
|
||||
.placeDestinationController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear, color: Colors.grey),
|
||||
onPressed: () {
|
||||
controller.placeDestinationController.clear();
|
||||
controller.placesDestination = [];
|
||||
controller.update();
|
||||
},
|
||||
)
|
||||
: (controller.polylines.isNotEmpty
|
||||
? IconButton(
|
||||
icon:
|
||||
const Icon(Icons.close, color: Colors.red),
|
||||
tooltip: 'إلغاء المسار',
|
||||
onPressed: () => controller.clearRoute(),
|
||||
)
|
||||
: null),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 15),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (controller.placesDestination.isNotEmpty)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxHeight: 220),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.85),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: controller.placesDestination.length,
|
||||
itemBuilder: (context, index) {
|
||||
final place = controller.placesDestination[index];
|
||||
return ListTile(
|
||||
title: Text(place['name'] ?? 'اسم غير معروف',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(place['address'] ?? '',
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
leading: const Icon(Icons.location_on_outlined,
|
||||
color: Colors.blue),
|
||||
onTap: () => controller.selectDestination(place),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- ويدجت خاصة بأزرار التحكم ---
|
||||
Widget _buildMapControls(NavigationController controller) {
|
||||
return Positioned(
|
||||
bottom: controller.currentInstruction.isNotEmpty ? 150 : 20,
|
||||
right: 12,
|
||||
child: Column(
|
||||
children: [
|
||||
if (controller.polylines.isNotEmpty) ...[
|
||||
FloatingActionButton(
|
||||
heroTag: 'rerouteBtn',
|
||||
mini: true,
|
||||
backgroundColor: Colors.white,
|
||||
tooltip: 'إعادة حساب المسار',
|
||||
onPressed: () => controller.recalculateRoute(),
|
||||
child: const Icon(Icons.sync_alt, color: Colors.blue),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
FloatingActionButton(
|
||||
heroTag: 'gpsBtn',
|
||||
mini: true,
|
||||
backgroundColor: Colors.white,
|
||||
onPressed: () {
|
||||
if (controller.myLocation != null) {
|
||||
controller.animateCameraToPosition(
|
||||
controller.myLocation!,
|
||||
bearing: controller.heading,
|
||||
zoom: 18.5,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.gps_fixed, color: Colors.black54),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- ويدجت خاصة بإرشادات الطريق المطورة ---
|
||||
Widget _buildNavigationInstruction(NavigationController controller) {
|
||||
return Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.blue.shade900, Colors.blue.shade600],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
],
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// --- الصف العلوي: السرعة والمسافة ---
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
controller.distanceToNextStep,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
controller.currentSpeed.toStringAsFixed(0),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Text(
|
||||
"كم/س",
|
||||
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(color: Colors.white38, height: 20, thickness: 0.8),
|
||||
// --- الصف السفلي: الإرشاد القادم ---
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.navigation_rounded,
|
||||
color: Colors.white, size: 32),
|
||||
const SizedBox(width: 15),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text("الخطوة التالية",
|
||||
style:
|
||||
TextStyle(color: Colors.white70, fontSize: 12)),
|
||||
Text(
|
||||
controller.nextInstruction.isNotEmpty
|
||||
? controller.nextInstruction
|
||||
: controller.currentInstruction,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ class MyTranslation extends Translations {
|
||||
"Cancel Trip": "إلغاء الرحلة",
|
||||
"Passenger Cancel Trip": "الراكب ألغى الرحلة",
|
||||
"VIP Order": "طلب VIP",
|
||||
'Hi ,I Arrive your site': "مرحبًا، لقد وصلت إلى موقعك",
|
||||
"The driver accepted your trip": "السائق قبل رحلتك",
|
||||
"message From passenger": "رسالة من الراكب",
|
||||
"Cancel": "إلغاء",
|
||||
@@ -34,7 +35,6 @@ class MyTranslation extends Translations {
|
||||
"نأسف لإعلامك بأن سائقًا آخر قد قبل هذا الطلب.",
|
||||
"Driver Applied the Ride for You": "السائق قدم الطلب لك",
|
||||
"Applied": "تم التقديم",
|
||||
"Hi ,I Arrive your site": "مرحبًا، لقد وصلت إلى موقعك",
|
||||
"Please go to Car Driver": "يرجى الذهاب إلى سائق السيارة",
|
||||
"Ok I will go now.": "حسنًا، سأذهب الآن.",
|
||||
"Accepted Ride": "تم قبول الرحلة",
|
||||
@@ -146,7 +146,11 @@ Raih Gai: For same-day return trips longer than 50km.
|
||||
رحّي غاي: للرحلات ذات العودة في نفس اليوم التي تزيد عن 50 كم.
|
||||
""",
|
||||
"I will go now": "هروح دلوقتي",
|
||||
"Yes": "أيوة",
|
||||
"Yes": "نعم",
|
||||
'Privacy Policy': "سياسة الخصوصية",
|
||||
'Ride info': "معلومات الرحلة",
|
||||
'you dont have accepted ride': "ليس لديك رحلة مقبولة",
|
||||
'Total Points': "إجمالي النقاط",
|
||||
"No,I want": "لا، أنا عاوز",
|
||||
"Your fee is": "المبلغ بتاعك هو",
|
||||
"Do you want to pay Tips for this Driver":
|
||||
@@ -424,7 +428,25 @@ Raih Gai: For same-day return trips longer than 50km.
|
||||
"color.beige": "بيج",
|
||||
"color.brown": "بني",
|
||||
"color.maroon": "خمري",
|
||||
'Ride History': "تاريخ الرحلات",
|
||||
"color.burgundy": "برغندي",
|
||||
'Name must be at least 2 characters':
|
||||
"الاسم يجب أن يكون على الأقل 2 حرف",
|
||||
'This Trip Was Cancelled': "تم إلغاء هذه الرحلة",
|
||||
'Trip Details': "تفاصيل الرحلة",
|
||||
'Could not load trip details.': "تعذر تحميل تفاصيل الرحلة.",
|
||||
'Trip Info': "معلومات الرحلة",
|
||||
'Order ID': "رقم الطلب",
|
||||
'Date': "التاريخ",
|
||||
'Earnings & Distance': "الأرباح والمسافة",
|
||||
'Trip Timeline': "جدول الرحلة",
|
||||
'Time to Passenger': "الوقت للراكب",
|
||||
'Trip Started': "بدأت الرحلة",
|
||||
'Trip Finished': "انتهت الرحلة",
|
||||
'Passenger & Status': "الراكب والحالة",
|
||||
'Status': "الحالة",
|
||||
'Passenger Name': "اسم الراكب",
|
||||
'National ID must be 11 digits': "الرقم الوطني يجب أن يكون 11 رقمًا",
|
||||
"color.yellow": "أصفر",
|
||||
"color.orange": "برتقالي",
|
||||
"color.gold": "ذهبي",
|
||||
@@ -756,8 +778,8 @@ Raih Gai: For same-day return trips longer than 50km.
|
||||
"لم نجد أي سائقين بعد. ضع في اعتبارك زيادة رسوم رحلتك لجعل عرضك أكثر جاذبية للسائقين.",
|
||||
"Allow Location Access": "السماح بالوصول إلى الموقع",
|
||||
"Show My Trip Count": "عرض عدد رحلاتي",
|
||||
"Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 8%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.":
|
||||
"انطلق هو التطبيق الأكثر أمانًا لمشاركة الركوب الذي يقدم العديد من الميزات لكل من السائقين والركاب. نحن نقدم أقل عمولة بنسبة 8% فقط، مما يضمن حصولك على أفضل قيمة لرحلاتك. يتضمن تطبيقنا التأمين لأفضل السائقين، الصيانة المنتظمة للسيارات مع أفضل المهندسين، والخدمات على الطريق لضمان تجربة محترمة وعالية الجودة لجميع المستخدمين.",
|
||||
"Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 15%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.":
|
||||
"انطلق هو التطبيق الأكثر أمانًا لمشاركة الركوب الذي يقدم العديد من الميزات لكل من السائقين والركاب. نحن نقدم أقل عمولة بنسبة 15% فقط، مما يضمن حصولك على أفضل قيمة لرحلاتك. يتضمن تطبيقنا التأمين لأفضل السائقين، الصيانة المنتظمة للسيارات مع أفضل المهندسين، والخدمات على الطريق لضمان تجربة محترمة وعالية الجودة لجميع المستخدمين.",
|
||||
"You can contact us during working hours from 12:00 - 19:00.":
|
||||
"يمكنك الاتصال بنا خلال ساعات العمل من 12:00 - 7:00.",
|
||||
"Show maintenance center near my location":
|
||||
@@ -1550,17 +1572,40 @@ Raih Gai: For same-day return trips longer than 50km.
|
||||
"otp verification failed": "رمز التحقق غير صحيح.",
|
||||
"registration failed": "فشلت عملية التسجيل.",
|
||||
"welcome user": "أهلاً بك، @firstName!",
|
||||
"Driver Wallet": "محفظة السائق",
|
||||
'Balance': 'الرصيد',
|
||||
"Today's Promo": "عرض اليوم",
|
||||
'Credit': 'رصيد', 'Debit': 'خصم',
|
||||
'Transactions this week': 'المعاملات هذا الأسبوع',
|
||||
'Weekly Summary': 'ملخص أسبوعي',
|
||||
'Total Weekly Earnings': 'إجمالي الأرباح الأسبوعية',
|
||||
'No transactions this week': 'لا توجد معاملات هذا الأسبوع',
|
||||
"Driver Balance": "رصيد السائق",
|
||||
"The 30000 points equal 30000 S.P for you \nSo go and gain your money":
|
||||
"الـ 30000 نقطة تساوي 30000 ل.س لك \nلذا اذهب واكسب أموالك",
|
||||
"OK": "موافق",
|
||||
"Your Application is Under Review": "طلبك قيد المراجعة",
|
||||
"We have received your application to join us as a driver. Our team is currently reviewing it. Thank you for your patience.":
|
||||
"لقد استلمنا طلبك للانضمام إلينا كسائق. يقوم فريقنا حاليًا بمراجعته. شكرًا لك على صبرك.",
|
||||
"You Will Be Notified": "سيتم إشعارك قريباً",
|
||||
"We will send you a notification as soon as your account is approved. You can safely close this page, and we'll let you know when the review is complete.":
|
||||
"سنرسل لك إشعاراً فور الموافقة على حسابك. يمكنك إغلاق هذه الصفحة بأمان، وسنعلمك عند اكتمال المراجعة.",
|
||||
"Refresh Status": "تحديث الحالة",
|
||||
"Checking for updates...": "جاري التحقق من التحديثات...",
|
||||
"Total Points is": "إجمالي النقاط هو",
|
||||
"Charge your Account": "اشحن حسابك",
|
||||
'''Types of Trips in Intaleq:
|
||||
|
||||
- Comfort: For cars newer than 2017 with air conditioning.
|
||||
- Lady: For girl drivers.
|
||||
- Speed: For fixed salary and endpoints.
|
||||
- Mashwari: For flexible trips where passengers choose the car and driver with prior arrangements.
|
||||
- Raih Gai: For same-day return trips longer than 50km.''':
|
||||
"أنواع الرحلات في انطلق:\n\n- مريح: للسيارات الأحدث من 2017 مع تكييف الهواء.\n- سيدة: للسائقات الإناث.\n- سرعة: لرحلات ذات راتب ثابت ونقاط نهاية محددة.\n- مشاوير: لرحلات مرنة حيث يختار الركاب السيارة والسائق مع ترتيبات مسبقة.\n- رايح جاي: لرحلات العودة في نفس اليوم لأكثر من 50 كم.",
|
||||
'L.S': 'ل.س',
|
||||
"Total Amount:": "المبلغ الإجمالي:",
|
||||
"Intaleq Wallet": "محفظة انطلق",
|
||||
"Intaleq Wallet": "رصيد انطلق",
|
||||
"Current Balance": "الرصيد الحالي",
|
||||
"S.P.": "ل.س.",
|
||||
"SYP": "ل.س.",
|
||||
"Your total balance:": "رصيدك الإجمالي:",
|
||||
"Payment Method:": "طريقة الدفع:",
|
||||
"e.g., 0912345678": "مثال: 0912345678",
|
||||
@@ -1643,23 +1688,12 @@ Raih Gai: For same-day return trips longer than 50km.
|
||||
"الشريك السائق في انتظارك في الموقع المُحدَّد .",
|
||||
"Pay with Your": "ادفع باستخدام",
|
||||
"Pay with Credit Card": "ادفع ببطاقة الائتمان",
|
||||
"Payment History": "سجل الدفعات",
|
||||
"Show Promos to Charge": "إظهار العروض الترويجية للشحن",
|
||||
"Point": "نقطة",
|
||||
"Driver Wallet": "محفظة الشريك السائق",
|
||||
"Total Points is": "رصيد التشغيل",
|
||||
"Total Budget from trips is": "الميزانية الإجمالية من الرحلات هي",
|
||||
"Total Amount:": "المبلغ الإجمالي:",
|
||||
"Total Budget from trips by": "الميزانية الإجمالية من الرحلات حسب",
|
||||
"Credit card is": "بطاقة الائتمان",
|
||||
"This amount for all trip I get from Passengers":
|
||||
"هذا المبلغ لجميع الرحلات التي أحصل عليها من الركاب",
|
||||
"Pay from my budget": "ادفع من ميزانيتي",
|
||||
// "This amount for all trip I get from Passengers and Collected For me in":
|
||||
// "هذا المبلغ لجميع الرحلات التي أحصل عليها من الركاب والتي تم جمعها من أجلي في",
|
||||
// "You can buy points from your budget":
|
||||
// "يمكنك شراء النقاط من ميزانيتك",
|
||||
// "insert amount": "أدخل المبلغ",
|
||||
|
||||
"You can buy Points to let you online":
|
||||
"يمكنك شراء النقاط لتمكينك من الدخول عبر الإنترنت",
|
||||
"by this list below": "من خلال هذه القائمة أدناه",
|
||||
@@ -2572,7 +2606,7 @@ Raih Gai: For same-day return trips longer than 50km.
|
||||
"Payment History": "Historique des paiements",
|
||||
"Show Promos to Charge": "Afficher les promotions d'expédition",
|
||||
"Point": "Points",
|
||||
"Driver Wallet": "Portefeuille chauffeur",
|
||||
"Driver Balance": "Portefeuille chauffeur",
|
||||
"Total Points is": "Le score total est de",
|
||||
"Total Budget from trips is": "Le budget total des voyages est de",
|
||||
"Total Amount:": "Montant total",
|
||||
@@ -3439,7 +3473,7 @@ Raih Gai: For same-day return trips longer than 50km.
|
||||
"Payment History": "Historique des paiements",
|
||||
"Show Promos to Charge": "Afficher les promotions d'expédition",
|
||||
"Point": "Points",
|
||||
"Driver Wallet": "Portefeuille chauffeur",
|
||||
"Driver Balance": "Portefeuille chauffeur",
|
||||
"Total Points is": "Le score total est de",
|
||||
"Total Budget from trips is": "Le budget total des voyages est de",
|
||||
"Total Amount:": "Montant total",
|
||||
@@ -4300,7 +4334,7 @@ Raih Gai: For same-day return trips longer than 50km.
|
||||
"Payment History": "Historique des paiements",
|
||||
"Show Promos to Charge": "Afficher les promotions d'expédition",
|
||||
"Point": "Points",
|
||||
"Driver Wallet": "Portefeuille chauffeur",
|
||||
"Driver Balance": "Portefeuille chauffeur",
|
||||
"Total Points is": "Le score total est de",
|
||||
"Total Budget from trips is": "Le budget total des voyages est de",
|
||||
"Total Amount:": "Montant total",
|
||||
@@ -5165,7 +5199,7 @@ Raih Gai: For same-day return trips longer than 50km.
|
||||
"Payment History": "Historique des paiements",
|
||||
"Show Promos to Charge": "Afficher les promotions d'expédition",
|
||||
"Point": "Points",
|
||||
"Driver Wallet": "Portefeuille chauffeur",
|
||||
"Driver Balance": "Portefeuille chauffeur",
|
||||
"Total Points is": "Le score total est de",
|
||||
"Total Budget from trips is": "Le budget total des voyages est de",
|
||||
"Total Amount:": "Montant total",
|
||||
@@ -6099,7 +6133,7 @@ Raih Gai: For same-day return trips longer than 50km.
|
||||
"Payment History": "Historique des paiements",
|
||||
"Show Promos to Charge": "Afficher les promotions d'expédition",
|
||||
"Point": "Points",
|
||||
"Driver Wallet": "Portefeuille chauffeur",
|
||||
"Driver Balance": "Portefeuille chauffeur",
|
||||
"Total Points is": "Le score total est de",
|
||||
"Total Budget from trips is": "Le budget total des voyages est de",
|
||||
"Total Amount:": "Montant total",
|
||||
@@ -6948,7 +6982,7 @@ Raih Gai: For same-day return trips longer than 50km.
|
||||
"Payment History": "भुगतान इतिहास",
|
||||
"Show Promos to Charge": "शिपिंग प्रमोशन दिखाएँ",
|
||||
"Point": "pt",
|
||||
"Driver Wallet": "ड्राइवर पार्टनर का वॉलेट",
|
||||
"Driver Balance": "ड्राइवर पार्टनर का वॉलेट",
|
||||
"Total Points is": "कुल स्कोर है",
|
||||
"Total Budget from trips is": "ट्रिप का कुल बजट है",
|
||||
"Total Amount:": "कुल राश",
|
||||
@@ -7796,7 +7830,7 @@ Raih Gai: For same-day return trips longer than 50km.
|
||||
"Show Promos to Charge": "نمایش تبلیغات حمل و نقل",
|
||||
"Point":
|
||||
"نقطه , خال , لکه , نقطه دار کردن , نوک , سر , نکته , ماده , اصل , موضوع , جهت , درجه , امتياز بازي , نمره درس , پوان , هدف , مسير , مرحله , قله , پايان , تيزکردن , گوشه دارکردن , نوکدار کردن , نوک گذاشتن (به) , خاطر نشان کردن , نشان دادن , متوجه ساختن , نقطه گذاري کردن , لک , لکه يا خال ميوه , ذره , لکه دار کردن , خالدار کردن",
|
||||
"Driver Wallet": "کیف پول راننده",
|
||||
"Driver Balance": "کیف پول راننده",
|
||||
"Total Points is": "مجموع امتیاز است",
|
||||
"Total Budget from trips is": "بودجه کل انطلقها می باشد",
|
||||
"Total Amount:": "مبلغ کل:",
|
||||
@@ -8560,7 +8594,7 @@ Raih Gai: For same-day return trips longer than 50km.
|
||||
"Payment History": "付款紀錄",
|
||||
"Show Promos to Charge": "顯示運送優惠",
|
||||
"Point": "一個點",
|
||||
"Driver Wallet": "職業駕駛錢包",
|
||||
"Driver Balance": "職業駕駛錢包",
|
||||
"Total Points is": "總分為",
|
||||
"Total Budget from trips is": "行程總預算為",
|
||||
"Total Amount:": "總金額:",
|
||||
|
||||
Reference in New Issue
Block a user