Initial commit for Intaleq Driver
This commit is contained in:
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cmake.sourceDirectory": "/Users/hamzaaleghwairyeen/development/App/intaleq_driver/linux"
|
||||
}
|
||||
@@ -44,10 +44,10 @@ android {
|
||||
applicationId = "com.intaleq_driver"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = 23
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = 8
|
||||
versionName = '1.0.8'
|
||||
minSdk = 29
|
||||
targetSdk = 36
|
||||
versionCode = 13
|
||||
versionName = '1.0.13'
|
||||
multiDexEnabled =true
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,14 @@
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:label="Intaleq Driver"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:allowBackup="false"
|
||||
android:fullBackupContent="false"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="false"
|
||||
android:theme="@style/LaunchTheme">
|
||||
|
||||
<activity
|
||||
|
||||
21
android/app/src/main/res/xml/network_security_config.xml
Normal file
21
android/app/src/main/res/xml/network_security_config.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
|
||||
<domain-config cleartextTrafficPermitted="false">
|
||||
<domain includeSubdomains="true">intaleq.xyz</domain>
|
||||
|
||||
<pin-set expiration="2027-01-01">
|
||||
<!-- <pin digest="SHA-256">pXmP2hTQLxDEvlTVmP5N7xpiA32sycBsxB6hBFT2uL4=</pin> -->
|
||||
<pin digest="SHA-256">XJXX7XthMj5VlSHfvo1q73sY7orJ9Wle0X4avj0/Vwo=</pin>
|
||||
|
||||
<pin digest="SHA-256">C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHESsl=</pin>
|
||||
</pin-set>
|
||||
</domain-config>
|
||||
|
||||
|
||||
</network-security-config>
|
||||
BIN
assets/alert.wav
Normal file
BIN
assets/alert.wav
Normal file
Binary file not shown.
@@ -12,6 +12,7 @@ class BoxName {
|
||||
static const String FCM_PRIVATE_KEY = "FCM_PRIVATE_KEY";
|
||||
static const String hmac = "hmac";
|
||||
static const String fingerPrint = "fingerPrint";
|
||||
static const String updateInterval = "updateInterval";
|
||||
static const String payMobApikey = "payMobApikey";
|
||||
static const String refreshToken = "refreshToken";
|
||||
static const String lang = "lang";
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -14,6 +14,7 @@ class AppLink {
|
||||
'https://walletintaleq.intaleq.xyz/v1/main';
|
||||
|
||||
static final String endPoint = 'https://intaleq.xyz/intaleq';
|
||||
static final String syria = 'https://syria.intaleq.xyz/intaleq';
|
||||
// 'https://api.tripz-egypt.com/tripz';
|
||||
static final String server = endPoint;
|
||||
static String seferCairoServer = endPoint;
|
||||
@@ -97,6 +98,7 @@ class AppLink {
|
||||
//-----------------ridessss------------------
|
||||
static String addRides = "$ride/rides/add.php";
|
||||
static String getRides = "$ride/rides/get.php";
|
||||
static String getPlacesSyria = "$ride/places_syria/get.php";
|
||||
static String getMishwari = "$ride/mishwari/get.php";
|
||||
static String getMishwariDriver = "$ride/mishwari/getDriver.php";
|
||||
static String getTripCountByCaptain = "$ride/rides/getTripCountByCaptain.php";
|
||||
@@ -250,6 +252,7 @@ class AppLink {
|
||||
static String uploadImage = "$server/uploadImage.php";
|
||||
static String uploadImage1 = "$server/uploadImage1.php";
|
||||
static String uploadImagePortrate = "$server/uploadImagePortrate.php";
|
||||
static String uploadSyrianDocs = "$syria/auth/syria/uploadSyrianDocs.php";
|
||||
static String uploadImageType = "$server/uploadImageType.php";
|
||||
//=============egypt documents ==============
|
||||
static String uploadEgyptidFront =
|
||||
@@ -310,6 +313,8 @@ class AppLink {
|
||||
static String updatePassengerGift = "$ride/invitor/updatePassengerGift.php";
|
||||
static String updateInvitationCodeFromRegister =
|
||||
"$ride/invitor/updateInvitationCodeFromRegister.php";
|
||||
static String register_driver_and_car =
|
||||
"$auth/syria/driver/register_driver_and_car.php";
|
||||
static String updateDriverInvitationDirectly =
|
||||
"$ride/invitor/updateDriverInvitationDirectly.php";
|
||||
static String updatePassengersInvitation =
|
||||
|
||||
@@ -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:": "總金額:",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/foundation.dart'
|
||||
show kIsWeb, defaultTargetPlatform, TargetPlatform;
|
||||
import 'controller/functions/device_analyzer.dart';
|
||||
|
||||
// --- CompatibilityDetailCard Widget (Updated to use 'max_score') ---
|
||||
// --- CompatibilityDetailCard Widget (كما هو) ---
|
||||
class CompatibilityDetailCard extends StatelessWidget {
|
||||
final Map<String, dynamic> detail;
|
||||
const CompatibilityDetailCard({super.key, required this.detail});
|
||||
@@ -16,17 +18,14 @@ class CompatibilityDetailCard extends StatelessWidget {
|
||||
|
||||
IconData _getIconForLabel(String label) {
|
||||
if (label.contains('رام')) return Icons.memory;
|
||||
if (label.contains('معالج') || label.contains('CPU')) {
|
||||
if (label.contains('معالج') || label.contains('CPU'))
|
||||
return Icons.developer_board;
|
||||
}
|
||||
if (label.contains('تخزين') || label.contains('كتابة')) {
|
||||
if (label.contains('تخزين') || label.contains('كتابة'))
|
||||
return Icons.sd_storage_outlined;
|
||||
}
|
||||
if (label.contains('أندرويد')) return Icons.android;
|
||||
if (label.contains('خدمات')) return Icons.g_mobiledata;
|
||||
if (label.contains('حساسات') || label.contains('Gyroscope')) {
|
||||
if (label.contains('حساسات') || label.contains('Gyroscope'))
|
||||
return Icons.sensors;
|
||||
}
|
||||
return Icons.smartphone;
|
||||
}
|
||||
|
||||
@@ -35,7 +34,6 @@ class CompatibilityDetailCard extends StatelessWidget {
|
||||
final bool status = detail['status'] ?? false;
|
||||
final String label = detail['label'] ?? "";
|
||||
final int achievedScore = detail['achieved_score'] ?? 0;
|
||||
// Corrected to use 'max_score' from the analyzer
|
||||
final int maxScore = detail['max_score'] ?? 1;
|
||||
final Color color = _getStatusColor(status, achievedScore, maxScore);
|
||||
final double progress =
|
||||
@@ -49,10 +47,9 @@ class CompatibilityDetailCard extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5),
|
||||
)
|
||||
color: Colors.grey.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 5))
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
@@ -64,20 +61,15 @@ class CompatibilityDetailCard extends StatelessWidget {
|
||||
color: Colors.grey.shade600, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
child: Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.grey.shade800,
|
||||
fontWeight: FontWeight.w600)),
|
||||
),
|
||||
Text("$achievedScore/$maxScore نقطة",
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: Colors.grey.shade800,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
// Corrected to display points out of max_score
|
||||
Text(
|
||||
"$achievedScore/$maxScore نقطة",
|
||||
style: TextStyle(
|
||||
color: color, fontWeight: FontWeight.bold, fontSize: 14),
|
||||
),
|
||||
color: color, fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -94,7 +86,7 @@ class CompatibilityDetailCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main Page Widget ---
|
||||
// --- Main Page Widget (Android-only) ---
|
||||
class DeviceCompatibilityPage extends StatefulWidget {
|
||||
const DeviceCompatibilityPage({super.key});
|
||||
@override
|
||||
@@ -107,23 +99,28 @@ class _DeviceCompatibilityPageState extends State<DeviceCompatibilityPage> {
|
||||
List<Map<String, dynamic>> details = [];
|
||||
bool isLoading = true;
|
||||
|
||||
bool get _isAndroid =>
|
||||
!kIsWeb && defaultTargetPlatform == TargetPlatform.android;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializePage();
|
||||
if (_isAndroid) {
|
||||
_initializePage();
|
||||
} else {
|
||||
// منصّة غير أندرويد: لا تعمل أي تحليلات
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initializePage() async {
|
||||
// await BatteryNotifier.checkBatteryAndNotify();
|
||||
final result = await DeviceAnalyzer().analyzeDevice();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
score = result['score'];
|
||||
details = List<Map<String, dynamic>>.from(result['details']);
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
score = result['score'];
|
||||
details = List<Map<String, dynamic>>.from(result['details']);
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Color _getColorForScore(int value) {
|
||||
@@ -141,6 +138,41 @@ class _DeviceCompatibilityPageState extends State<DeviceCompatibilityPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// حظر الصفحة على غير أندرويد
|
||||
if (!_isAndroid) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F8FC),
|
||||
appBar: AppBar(
|
||||
title: const Text("توافق الجهاز",
|
||||
style: TextStyle(
|
||||
color: Colors.black87, fontWeight: FontWeight.bold)),
|
||||
centerTitle: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
iconTheme: const IconThemeData(color: Colors.black87),
|
||||
),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.phone_iphone, size: 56, color: Colors.grey),
|
||||
const SizedBox(height: 12),
|
||||
const Text("هذه الصفحة متاحة لأجهزة أندرويد فقط",
|
||||
style: TextStyle(fontSize: 16)),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text("رجوع"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F8FC),
|
||||
appBar: AppBar(
|
||||
@@ -188,57 +220,48 @@ class _DeviceCompatibilityPageState extends State<DeviceCompatibilityPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// ## Corrected Score Header Widget
|
||||
/// This widget now uses a `Stack` to correctly place the text over the `PieChart`.
|
||||
/// الهيدر
|
||||
Widget _buildScoreHeader() {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
height: 220, // Give the container a fixed height
|
||||
height: 220,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Layer 1: The Pie Chart
|
||||
PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 4,
|
||||
// This creates the "hole" in the middle.
|
||||
centerSpaceRadius: 80,
|
||||
startDegreeOffset: -90,
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
color: _getColorForScore(score),
|
||||
value: score.toDouble(),
|
||||
title: '',
|
||||
radius: 25,
|
||||
),
|
||||
color: _getColorForScore(score),
|
||||
value: score.toDouble(),
|
||||
title: '',
|
||||
radius: 25),
|
||||
PieChartSectionData(
|
||||
color: Colors.grey.shade200,
|
||||
value: (100 - score).toDouble().clamp(0, 100),
|
||||
title: '',
|
||||
radius: 25,
|
||||
),
|
||||
color: Colors.grey.shade200,
|
||||
value: (100 - score).toDouble().clamp(0, 100),
|
||||
title: '',
|
||||
radius: 25),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Layer 2: The text and message, centered on top of the chart
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"$score%",
|
||||
style: TextStyle(
|
||||
fontSize: 52,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getColorForScore(score)),
|
||||
),
|
||||
Text("$score%",
|
||||
style: TextStyle(
|
||||
fontSize: 52,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getColorForScore(score))),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_getScoreMessage(score),
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade700,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(_getScoreMessage(score),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade700,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500)),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:sefer_driver/views/home/Captin/orderCaptin/order_request_page.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
@@ -17,7 +18,9 @@ import 'constant/api_key.dart';
|
||||
import 'constant/info.dart';
|
||||
import 'controller/firebase/firbase_messge.dart';
|
||||
import 'controller/firebase/local_notification.dart';
|
||||
import 'controller/functions/add_error.dart';
|
||||
import 'controller/functions/battery_status.dart';
|
||||
import 'controller/functions/crud.dart';
|
||||
import 'controller/functions/encrypt_decrypt.dart';
|
||||
import 'controller/functions/secure_storage.dart';
|
||||
import 'controller/local/local_controller.dart';
|
||||
@@ -134,13 +137,22 @@ void main() async {
|
||||
|
||||
await WakelockPlus.enable();
|
||||
await GetStorage.init();
|
||||
await initializeDateFormatting();
|
||||
Stripe.publishableKey = AK.publishableKeyStripe;
|
||||
SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
|
||||
runApp(const MyApp());
|
||||
runZonedGuarded<Future<void>>(() async {
|
||||
// ... الكود الحالي الموجود في دالة main ...
|
||||
runApp(const MyApp());
|
||||
}, (error, stack) {
|
||||
// أي خطأ غير متوقع في التطبيق سيتم التقاطه هنا CRUD.
|
||||
print("Caught Dart error: $error");
|
||||
print(stack);
|
||||
// أرسل الخطأ إلى السيرفر
|
||||
CRUD.addError(error.toString(), stack.toString(), 'main');
|
||||
});
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
|
||||
@@ -240,7 +240,7 @@ class RatePassenger extends StatelessWidget {
|
||||
RatingBar.builder(
|
||||
initialRating: 0,
|
||||
itemCount: 5,
|
||||
itemSize: 50,
|
||||
itemSize: 40,
|
||||
itemPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
itemBuilder: (context, index) {
|
||||
switch (index) {
|
||||
|
||||
@@ -35,7 +35,7 @@ class ContactUsPage extends StatelessWidget {
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
Get.put(TextToSpeechController()).speakText(
|
||||
'Tripz 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.'
|
||||
'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.'
|
||||
.tr);
|
||||
},
|
||||
icon: const Icon(Icons.headphones),
|
||||
@@ -43,7 +43,7 @@ class ContactUsPage extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Tripz 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.'
|
||||
'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.'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
|
||||
@@ -8,10 +8,12 @@ import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/auth/apple_sigin.dart';
|
||||
import '../../../controller/auth/captin/login_captin_controller.dart';
|
||||
@@ -356,7 +358,12 @@ class LoginCaptin extends StatelessWidget {
|
||||
decoration: TextDecoration.underline,
|
||||
color: AppColor.blueColor,
|
||||
fontWeight: FontWeight.bold),
|
||||
recognizer: TapGestureRecognizer()..onTap = () {}),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrl(
|
||||
Uri.parse('${AppLink.server}/privacy_policy.php'),
|
||||
mode: LaunchMode.externalApplication);
|
||||
}),
|
||||
TextSpan(text: " and acknowledge our Privacy Policy.".tr),
|
||||
],
|
||||
),
|
||||
@@ -369,7 +376,9 @@ class LoginCaptin extends StatelessWidget {
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: HtmlWidget(AppInformation.privacyPolicyArabic),
|
||||
child: HtmlWidget(box.read(BoxName.lang).toString() == 'ar'
|
||||
? AppInformation.privacyPolicyArabic
|
||||
: AppInformation.privacyPolicy),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
167
lib/views/auth/syria/pending_driver_page.dart
Normal file
167
lib/views/auth/syria/pending_driver_page.dart
Normal file
@@ -0,0 +1,167 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class DriverVerificationScreen extends StatefulWidget {
|
||||
const DriverVerificationScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DriverVerificationScreen> createState() =>
|
||||
_DriverVerificationScreenState();
|
||||
}
|
||||
|
||||
class _DriverVerificationScreenState extends State<DriverVerificationScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 2),
|
||||
)..repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FA),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Animated Icon
|
||||
AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (_, child) {
|
||||
return Transform.rotate(
|
||||
angle: _controller.value * 2 * math.pi,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.sync,
|
||||
size: 80,
|
||||
color: theme.primaryColor.withOpacity(0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Title
|
||||
Text(
|
||||
"Your Application is Under Review".tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.textTheme.titleLarge?.color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Main Message
|
||||
Text(
|
||||
"We have received your application to join us as a driver. Our team is currently reviewing it. Thank you for your patience."
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black54,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Notification Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: theme.primaryColor.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.notifications_active_outlined,
|
||||
color: theme.primaryColor, size: 30),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"You Will Be Notified".tr,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.primaryColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"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."
|
||||
.tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black87,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Refresh Button
|
||||
// ElevatedButton.icon(
|
||||
// onPressed: () {
|
||||
// // TODO: Add logic to check status from your API
|
||||
// Get.snackbar(
|
||||
// "Status", // This can also be a key if you want
|
||||
// "Checking for updates...".tr,
|
||||
// snackPosition: SnackPosition.BOTTOM,
|
||||
// );
|
||||
// },
|
||||
// icon: const Icon(Icons.refresh, color: Colors.white),
|
||||
// label: Text(
|
||||
// "Refresh Status".tr,
|
||||
// style: const TextStyle(fontSize: 16, color: Colors.white),
|
||||
// ),
|
||||
// style: ElevatedButton.styleFrom(
|
||||
// backgroundColor: theme.primaryColor,
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// horizontal: 40, vertical: 15),
|
||||
// shape: RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadius.circular(30.0),
|
||||
// ),
|
||||
// elevation: 3,
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -72,29 +72,53 @@ class RegistrationView extends StatelessWidget {
|
||||
TextFormField(
|
||||
controller: c.firstNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'First Name'.tr,
|
||||
border: const OutlineInputBorder()),
|
||||
validator: (v) =>
|
||||
(v?.isEmpty ?? true) ? 'Required field'.tr : null,
|
||||
labelText: 'First Name'.tr,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) {
|
||||
return 'Required field'.tr;
|
||||
}
|
||||
if (v.length < 2) {
|
||||
return 'Name must be at least 2 characters'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: c.lastNameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Last Name'.tr,
|
||||
border: const OutlineInputBorder()),
|
||||
validator: (v) =>
|
||||
(v?.isEmpty ?? true) ? 'Required field'.tr : null,
|
||||
labelText: 'Last Name'.tr,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) {
|
||||
return 'Required field'.tr;
|
||||
}
|
||||
if (v.length < 2) {
|
||||
return 'Name must be at least 2 characters'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: c.nationalIdController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'National ID Number'.tr,
|
||||
border: const OutlineInputBorder()),
|
||||
labelText: 'National ID Number'.tr,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (v) =>
|
||||
(v?.isEmpty ?? true) ? 'Required field'.tr : null,
|
||||
validator: (v) {
|
||||
if (v == null || v.isEmpty) {
|
||||
return 'Required field'.tr;
|
||||
}
|
||||
if (v.length != 11) {
|
||||
return 'National ID must be 11 digits'.tr;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
@@ -296,6 +320,15 @@ class RegistrationView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget signedImageWithAuth(String fileUrl, String bearerToken) {
|
||||
return Image.network(
|
||||
fileUrl,
|
||||
headers: {'Authorization': 'Bearer $bearerToken'},
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) => const Text('Image expired or unauthorized'),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImagePickerBox(String title, File? img, VoidCallback onTap) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
|
||||
@@ -26,7 +26,7 @@ class AboutPage extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'SEFER LLC\n${box.read(BoxName.countryCode).toString().tr}',
|
||||
'Intaleq LLC\n${'Syria'.tr}',
|
||||
style: AppStyle.headTitle2,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@@ -34,7 +34,7 @@ class AboutPage extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Text(
|
||||
'SEFER is a ride-sharing app designed with your safety and affordability in mind. We connect you with reliable drivers in your area, ensuring a convenient and stress-free travel experience.\n\nHere are some of the key features that set us apart:'
|
||||
'Intaleq is a ride-sharing app designed with your safety and affordability in mind. We connect you with reliable drivers in your area, ensuring a convenient and stress-free travel experience.\n\nHere are some of the key features that set us apart:'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../../Rate/rate_passenger.dart';
|
||||
import '../../widgets/my_textField.dart';
|
||||
import 'mapDriverWidgets/driver_end_ride_bar.dart';
|
||||
import 'mapDriverWidgets/google_driver_map_page.dart';
|
||||
import 'mapDriverWidgets/google_map_app.dart';
|
||||
import 'mapDriverWidgets/passenger_info_window.dart';
|
||||
import 'mapDriverWidgets/sos_connect.dart';
|
||||
|
||||
@@ -44,12 +45,13 @@ class PassengerLocationMapPage extends StatelessWidget {
|
||||
// 2. شريط تعليمات الطريق في الأعلى
|
||||
const InstructionsOfRoads(),
|
||||
|
||||
// 3. زر إلغاء الرحلة في الأعلى يسارًا
|
||||
CancelWidget(mapDriverController: mapDriverController),
|
||||
|
||||
// 4. نافذة معلومات الراكب في الأسفل (تظهر قبل بدء الرحلة)
|
||||
const PassengerInfoWindow(),
|
||||
|
||||
const PassengerInfoWindow(),
|
||||
// 3. زر إلغاء الرحلة في الأعلى يسارًا
|
||||
|
||||
CancelWidget(mapDriverController: mapDriverController),
|
||||
// Changed: تم تعديل تصميم زر الإلغاء ليكون أيقونة بسيطة في الأعلى
|
||||
// 5. شريط معلومات وإنهاء الرحلة (يظهر بعد بدء الرحلة)
|
||||
driverEndRideBar(),
|
||||
|
||||
@@ -58,7 +60,7 @@ class PassengerLocationMapPage extends StatelessWidget {
|
||||
|
||||
// 7. دائرة عرض السرعة
|
||||
speedCircle(),
|
||||
|
||||
GoogleMapApp(),
|
||||
// 8. نافذة عرض السعر النهائي (تظهر بعد انتهاء الرحلة)
|
||||
const PricesWindow(),
|
||||
],
|
||||
@@ -131,7 +133,7 @@ class CancelWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: 10,
|
||||
top: 70,
|
||||
left: 10,
|
||||
child: GetBuilder<MapDriverController>(
|
||||
builder: (controller) {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
|
||||
import '../../../../controller/auth/captin/history_captain.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../controller/functions/encrypt_decrypt.dart';
|
||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||
|
||||
class HistoryCaptain extends StatelessWidget {
|
||||
const HistoryCaptain({super.key});
|
||||
@@ -14,136 +11,221 @@ class HistoryCaptain extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(HistoryCaptainController());
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text('History Page'.tr),
|
||||
leading: CupertinoNavigationBarBackButton(
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[100], // A softer background color
|
||||
appBar: AppBar(
|
||||
title: Text('Ride History'.tr),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
elevation: 1,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: GetBuilder<HistoryCaptainController>(
|
||||
builder: (historyCaptainController) => historyCaptainController
|
||||
.isloading
|
||||
? const Center(child: CupertinoActivityIndicator())
|
||||
: historyCaptainController.historyData['message'].length < 1
|
||||
? Center(
|
||||
child: Text(
|
||||
'No ride Yet.'.tr,
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.navTitleTextStyle,
|
||||
body: GetBuilder<HistoryCaptainController>(
|
||||
builder: (controller) {
|
||||
if (controller.isloading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.historyData['message'].isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.history_toggle_off,
|
||||
size: 80, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No Rides Yet'.tr,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 动画: Wrap ListView with AnimationLimiter for staggered animations
|
||||
return AnimationLimiter(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
itemCount: controller.historyData['message'].length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var trip = controller.historyData['message'][index];
|
||||
|
||||
// 动画: Apply animation to each list item
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: _AnimatedHistoryCard(
|
||||
trip: trip,
|
||||
onTap: () {
|
||||
// Your original logic is preserved here
|
||||
if (trip['status'] != 'Cancel') {
|
||||
controller.getHistoryDetails(trip['order_id']);
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'This Trip Was Cancelled'.tr,
|
||||
'This Trip Was Cancelled'.tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: historyCaptainController
|
||||
.historyData['message'].length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var list = historyCaptainController
|
||||
.historyData['message'][index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: CupertinoColors.systemGrey, width: 1),
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.circular(8.0)),
|
||||
),
|
||||
child: CupertinoButton(
|
||||
onPressed: () {
|
||||
if (list['status'] != 'Cancel') {
|
||||
historyCaptainController
|
||||
.getHistoryDetails(list['order_id']);
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'This Trip Cancelled'.tr,
|
||||
'This Trip Cancelled'.tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'OrderId'.tr,
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.navTitleTextStyle,
|
||||
),
|
||||
Text(
|
||||
EncryptionHelper.instance
|
||||
.decryptData(list['order_id']),
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.textStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'created time'.tr,
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.navTitleTextStyle,
|
||||
),
|
||||
Text(
|
||||
list['created_at'],
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.textStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
list['status'],
|
||||
style: EncryptionHelper.instance
|
||||
.decryptData(
|
||||
list['status']) ==
|
||||
'Apply'
|
||||
? CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.navTitleTextStyle
|
||||
.copyWith(
|
||||
color: CupertinoColors
|
||||
.systemGreen)
|
||||
: EncryptionHelper.instance.decryptData(
|
||||
list['status']) ==
|
||||
'Refused'
|
||||
? CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.navTitleTextStyle
|
||||
.copyWith(
|
||||
color: CupertinoColors
|
||||
.systemRed)
|
||||
: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.navTitleTextStyle
|
||||
.copyWith(
|
||||
color: CupertinoColors
|
||||
.systemYellow),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 动画: A new stateful widget to handle the tap animation
|
||||
class _AnimatedHistoryCard extends StatefulWidget {
|
||||
final Map<String, dynamic> trip;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _AnimatedHistoryCard({required this.trip, required this.onTap});
|
||||
|
||||
@override
|
||||
__AnimatedHistoryCardState createState() => __AnimatedHistoryCardState();
|
||||
}
|
||||
|
||||
class __AnimatedHistoryCardState extends State<_AnimatedHistoryCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _scaleAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
);
|
||||
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTapDown(TapDownDetails details) {
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
void _onTapUp(TapUpDetails details) {
|
||||
_controller.reverse();
|
||||
widget.onTap();
|
||||
}
|
||||
|
||||
void _onTapCancel() {
|
||||
_controller.reverse();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTapDown: _onTapDown,
|
||||
onTapUp: _onTapUp,
|
||||
onTapCancel: _onTapCancel,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleAnimation,
|
||||
child: Card(
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.receipt_long,
|
||||
color: Theme.of(context).primaryColor, size: 40),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${'OrderId'.tr}: ${widget.trip['order_id']}',
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.trip['created_at'],
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildStatusChip(widget.trip['status']),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🎨 A separate function for the status chip, slightly restyled for Material
|
||||
Widget _buildStatusChip(String status) {
|
||||
Color chipColor;
|
||||
Color textColor;
|
||||
String statusText = status;
|
||||
IconData iconData;
|
||||
|
||||
switch (status) {
|
||||
case 'Apply':
|
||||
chipColor = Colors.green.shade50;
|
||||
textColor = Colors.green.shade800;
|
||||
iconData = Icons.check_circle;
|
||||
break;
|
||||
case 'Refused':
|
||||
chipColor = Colors.red.shade50;
|
||||
textColor = Colors.red.shade800;
|
||||
iconData = Icons.cancel;
|
||||
break;
|
||||
case 'Cancel':
|
||||
chipColor = Colors.orange.shade50;
|
||||
textColor = Colors.orange.shade800;
|
||||
iconData = Icons.info;
|
||||
statusText = 'Cancelled';
|
||||
break;
|
||||
default:
|
||||
chipColor = Colors.grey.shade200;
|
||||
textColor = Colors.grey.shade800;
|
||||
iconData = Icons.hourglass_empty;
|
||||
}
|
||||
|
||||
return Chip(
|
||||
avatar: Icon(iconData, color: textColor, size: 16),
|
||||
label: Text(
|
||||
statusText.tr,
|
||||
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: chipColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
side: BorderSide.none,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,252 +1,369 @@
|
||||
import 'package:sefer_driver/controller/functions/location_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
|
||||
import 'package:sefer_driver/controller/auth/captin/history_captain.dart';
|
||||
import 'package:sefer_driver/controller/functions/launch.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
class HistoryDetailsPage extends StatefulWidget {
|
||||
const HistoryDetailsPage({super.key});
|
||||
|
||||
import '../../../../controller/functions/encrypt_decrypt.dart';
|
||||
@override
|
||||
State<HistoryDetailsPage> createState() => _HistoryDetailsPageState();
|
||||
}
|
||||
|
||||
class HistoryDetailsPage extends StatelessWidget {
|
||||
HistoryDetailsPage({super.key});
|
||||
HistoryCaptainController historyCaptainController =
|
||||
Get.put(HistoryCaptainController());
|
||||
class _HistoryDetailsPageState extends State<HistoryDetailsPage> {
|
||||
// Get the controller instance
|
||||
final HistoryCaptainController controller =
|
||||
Get.find<HistoryCaptainController>();
|
||||
|
||||
// Helper method to safely parse LatLng from a string 'lat,lng'
|
||||
LatLng? _parseLatLng(String? latLngString) {
|
||||
if (latLngString == null) return null;
|
||||
final parts = latLngString.split(',');
|
||||
if (parts.length != 2) return null;
|
||||
final lat = double.tryParse(parts[0]);
|
||||
final lng = double.tryParse(parts[1]);
|
||||
if (lat == null || lng == null) return null;
|
||||
return LatLng(lat, lng);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text('Trip Detail'.tr),
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Icon(CupertinoIcons.back),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
title: Text('Trip Details'.tr),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 1,
|
||||
),
|
||||
child: GetBuilder<HistoryCaptainController>(
|
||||
builder: (historyCaptainController) {
|
||||
var res = historyCaptainController.historyDetailsData['data'];
|
||||
return historyCaptainController.isloading
|
||||
? const Center(
|
||||
child: CupertinoActivityIndicator(),
|
||||
)
|
||||
: CupertinoScrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
CupertinoButton(
|
||||
onPressed: () {
|
||||
String mapUrl =
|
||||
'https://www.google.com/maps/dir/${EncryptionHelper.instance.decryptData(res['start_location'])}/${EncryptionHelper.instance.decryptData(res['end_location'])}/';
|
||||
showInBrowser(mapUrl);
|
||||
},
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
border: Border.all(
|
||||
color: CupertinoColors.activeBlue,
|
||||
width: 2),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
SizedBox(
|
||||
height: MediaQuery.of(context).size.height *
|
||||
0.3,
|
||||
child: GoogleMap(
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: Get.find<LocationController>()
|
||||
.myLocation,
|
||||
tilt: 80,
|
||||
zoom: 13,
|
||||
),
|
||||
zoomControlsEnabled: true,
|
||||
polylines: {
|
||||
Polyline(
|
||||
polylineId: const PolylineId('route'),
|
||||
points: [
|
||||
LatLng(
|
||||
double.parse(EncryptionHelper
|
||||
.instance
|
||||
.decryptData(
|
||||
res['start_location'])
|
||||
.toString()
|
||||
.split(',')[0]),
|
||||
double.parse(EncryptionHelper
|
||||
.instance
|
||||
.decryptData(
|
||||
res['start_location'])
|
||||
.toString()
|
||||
.split(',')[1]),
|
||||
),
|
||||
LatLng(
|
||||
double.parse(EncryptionHelper
|
||||
.instance
|
||||
.decryptData(
|
||||
res['end_location'])
|
||||
.toString()
|
||||
.split(',')[0]),
|
||||
double.parse(EncryptionHelper
|
||||
.instance
|
||||
.decryptData(
|
||||
res['end_location'])
|
||||
.toString()
|
||||
.split(',')[1]),
|
||||
)
|
||||
],
|
||||
color: CupertinoColors.activeGreen,
|
||||
width: 5,
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Order ID'.tr} ${EncryptionHelper.instance.decryptData(res['id'])}',
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.navActionTextStyle,
|
||||
),
|
||||
Text(
|
||||
res['date'].toString(),
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.navActionTextStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
border: Border.all(
|
||||
color: CupertinoColors.activeGreen, width: 2),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Price is'.tr} ${EncryptionHelper.instance.decryptData(res['price_for_driver'])}',
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.textStyle,
|
||||
),
|
||||
Text(
|
||||
'${'Distance is'.tr} ${EncryptionHelper.instance.decryptData(res['distance'])} KM',
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.textStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Times of Trip'.tr,
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.navTitleTextStyle,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
border: Border.all(
|
||||
color: CupertinoColors.destructiveRed,
|
||||
width: 2),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'${'Time to Passenger is'.tr} ${res['DriverIsGoingToPassenger']}',
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.textStyle,
|
||||
),
|
||||
Text(
|
||||
'${'TimeStart is'.tr} ${res['rideTimeStart']}',
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.textStyle,
|
||||
),
|
||||
Text(
|
||||
'${'Time Finish is'.tr} ${res['rideTimeFinish']}',
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.textStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
border: Border.all(
|
||||
color: CupertinoColors.systemGreen, width: 2),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${'Passenger Name is'.tr} ${EncryptionHelper.instance.decryptData(res['first_name'])} ${EncryptionHelper.instance.decryptData(res['last_name'])}',
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.textStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * 0.9,
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
border: Border.all(
|
||||
color: CupertinoColors.systemYellow,
|
||||
width: 2),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${'Status is'.tr} ${EncryptionHelper.instance.decryptData(res['status'])}',
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.textStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: GetBuilder<HistoryCaptainController>(
|
||||
builder: (controller) {
|
||||
if (controller.isloading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
final res = controller.historyDetailsData['data'];
|
||||
if (res == null) {
|
||||
return Center(child: Text('Could not load trip details.'.tr));
|
||||
}
|
||||
|
||||
final startLocation = _parseLatLng(res['start_location']);
|
||||
final endLocation = _parseLatLng(res['end_location']);
|
||||
|
||||
// Create markers for the map
|
||||
final Set<Marker> markers = {};
|
||||
if (startLocation != null) {
|
||||
markers.add(Marker(
|
||||
markerId: const MarkerId('start'),
|
||||
position: startLocation,
|
||||
infoWindow: InfoWindow(title: 'Start'.tr)));
|
||||
}
|
||||
if (endLocation != null) {
|
||||
markers.add(Marker(
|
||||
markerId: const MarkerId('end'),
|
||||
position: endLocation,
|
||||
infoWindow: InfoWindow(title: 'End'.tr)));
|
||||
}
|
||||
|
||||
return AnimationLimiter(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: AnimationConfiguration.toStaggeredList(
|
||||
duration: const Duration(milliseconds: 375),
|
||||
childAnimationBuilder: (widget) => SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(child: widget),
|
||||
),
|
||||
children: [
|
||||
// --- Map Card ---
|
||||
_buildMapCard(context, startLocation, endLocation, markers),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// --- Trip Info Card ---
|
||||
_DetailCard(
|
||||
icon: Icons.receipt_long,
|
||||
title: 'Trip Info'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_InfoTile(
|
||||
label: 'Order ID'.tr,
|
||||
value: res['id']?.toString() ?? 'N/A'),
|
||||
_InfoTile(
|
||||
label: 'Date'.tr,
|
||||
value: res['date']?.toString() ?? 'N/A'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// --- Earnings Card ---
|
||||
_DetailCard(
|
||||
icon: Icons.account_balance_wallet,
|
||||
title: 'Earnings & Distance'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_InfoTile(
|
||||
label: 'Your Earnings'.tr,
|
||||
value: '${res['price_for_driver']}'),
|
||||
_InfoTile(
|
||||
label: 'Distance'.tr,
|
||||
value: '${res['distance']} KM'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- Timeline Card ---
|
||||
_DetailCard(
|
||||
icon: Icons.timeline,
|
||||
title: 'Trip Timeline'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_InfoTile(
|
||||
label: 'Time to Passenger'.tr,
|
||||
value: res['DriverIsGoingToPassenger'] ?? 'N/A'),
|
||||
_InfoTile(
|
||||
label: 'Trip Started'.tr,
|
||||
value: res['rideTimeStart'] ?? 'N/A'),
|
||||
_InfoTile(
|
||||
label: 'Trip Finished'.tr,
|
||||
value: res['rideTimeFinish'] ?? 'N/A'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// --- Passenger & Status Card ---
|
||||
_DetailCard(
|
||||
icon: Icons.person,
|
||||
title: 'Passenger & Status'.tr,
|
||||
child: Column(
|
||||
children: [
|
||||
_InfoTile(
|
||||
label: 'Passenger Name'.tr,
|
||||
value:
|
||||
'${res['passengerName']} ${res['last_name']}'),
|
||||
_InfoTile(
|
||||
label: 'Status'.tr,
|
||||
value: res['status'] ?? 'N/A',
|
||||
isStatus: true),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMapCard(BuildContext context, LatLng? startLocation,
|
||||
LatLng? endLocation, Set<Marker> markers) {
|
||||
// A fallback position if locations are not available
|
||||
final initialCameraPosition = (startLocation != null)
|
||||
? CameraPosition(target: startLocation, zoom: 14)
|
||||
: const CameraPosition(
|
||||
target: LatLng(31.96, 35.92), zoom: 12); // Fallback to Amman
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
clipBehavior:
|
||||
Clip.antiAlias, // Ensures the map respects the border radius
|
||||
child: Stack(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 250,
|
||||
child: GoogleMap(
|
||||
initialCameraPosition: initialCameraPosition,
|
||||
markers: markers,
|
||||
polylines: {
|
||||
if (startLocation != null && endLocation != null)
|
||||
Polyline(
|
||||
polylineId: const PolylineId('route'),
|
||||
points: [startLocation, endLocation],
|
||||
color: Colors.deepPurple,
|
||||
width: 5,
|
||||
),
|
||||
},
|
||||
onMapCreated: (GoogleMapController mapController) {
|
||||
// Animate camera to fit the route
|
||||
if (startLocation != null && endLocation != null) {
|
||||
LatLngBounds bounds = LatLngBounds(
|
||||
southwest: LatLng(
|
||||
startLocation.latitude < endLocation.latitude
|
||||
? startLocation.latitude
|
||||
: endLocation.latitude,
|
||||
startLocation.longitude < endLocation.longitude
|
||||
? startLocation.longitude
|
||||
: endLocation.longitude,
|
||||
),
|
||||
northeast: LatLng(
|
||||
startLocation.latitude > endLocation.latitude
|
||||
? startLocation.latitude
|
||||
: endLocation.latitude,
|
||||
startLocation.longitude > endLocation.longitude
|
||||
? startLocation.longitude
|
||||
: endLocation.longitude,
|
||||
),
|
||||
);
|
||||
mapController.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(bounds, 60.0));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 10,
|
||||
right: 10,
|
||||
child: FloatingActionButton.small(
|
||||
heroTag: 'open_maps',
|
||||
onPressed: () {
|
||||
if (startLocation != null && endLocation != null) {
|
||||
String mapUrl =
|
||||
'https://www.google.com/maps/dir/${startLocation.latitude},${startLocation.longitude}/${endLocation.latitude},${endLocation.longitude}/';
|
||||
showInBrowser(mapUrl);
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.directions),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A reusable widget for the main detail cards
|
||||
class _DetailCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final Widget child;
|
||||
|
||||
const _DetailCard({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withOpacity(0.05),
|
||||
margin: const EdgeInsets.only(bottom: 16.0),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: Theme.of(context).primaryColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleLarge
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A reusable widget for a label-value pair inside a card
|
||||
class _InfoTile extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final bool isStatus;
|
||||
|
||||
const _InfoTile({
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.isStatus = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(color: Colors.grey[700])),
|
||||
if (isStatus)
|
||||
_buildStatusChip(value)
|
||||
else
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Reusing the status chip from the previous page for consistency
|
||||
Widget _buildStatusChip(String status) {
|
||||
Color chipColor;
|
||||
Color textColor;
|
||||
IconData iconData;
|
||||
|
||||
switch (status.toLowerCase()) {
|
||||
case 'apply':
|
||||
case 'completed': // Assuming 'Apply' means completed
|
||||
chipColor = Colors.green.shade50;
|
||||
textColor = Colors.green.shade800;
|
||||
iconData = Icons.check_circle;
|
||||
status = 'Completed';
|
||||
break;
|
||||
case 'refused':
|
||||
chipColor = Colors.red.shade50;
|
||||
textColor = Colors.red.shade800;
|
||||
iconData = Icons.cancel;
|
||||
break;
|
||||
case 'cancel':
|
||||
chipColor = Colors.orange.shade50;
|
||||
textColor = Colors.orange.shade800;
|
||||
iconData = Icons.info;
|
||||
status = 'Cancelled';
|
||||
break;
|
||||
default:
|
||||
chipColor = Colors.grey.shade200;
|
||||
textColor = Colors.grey.shade800;
|
||||
iconData = Icons.hourglass_empty;
|
||||
}
|
||||
|
||||
return Chip(
|
||||
avatar: Icon(iconData, color: textColor, size: 16),
|
||||
label: Text(
|
||||
status.tr,
|
||||
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: chipColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
side: BorderSide.none,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
@@ -20,6 +22,7 @@ import 'package:sefer_driver/views/home/Captin/About Us/settings_captain.dart';
|
||||
import 'package:sefer_driver/views/home/my_wallet/walet_captain.dart';
|
||||
import 'package:sefer_driver/views/home/profile/profile_captain.dart';
|
||||
import 'package:sefer_driver/views/notification/notification_captain.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../About Us/video_page.dart';
|
||||
import '../assurance_health_page.dart';
|
||||
import '../maintain_center_page.dart';
|
||||
@@ -47,7 +50,7 @@ class AppDrawer extends StatelessWidget {
|
||||
// 2. تعريف بيانات القائمة بشكل مركزي ومنظم
|
||||
final List<DrawerItem> drawerItems = [
|
||||
DrawerItem(
|
||||
title: 'Wallet'.tr,
|
||||
title: 'Balance'.tr,
|
||||
icon: Icons.account_balance_wallet,
|
||||
color: Colors.green,
|
||||
onTap: () => Get.to(() => WalletCaptainRefactored())),
|
||||
@@ -81,16 +84,16 @@ class AppDrawer extends StatelessWidget {
|
||||
icon: Icons.share,
|
||||
color: Colors.indigo,
|
||||
onTap: () => Get.to(() => InviteScreen())),
|
||||
DrawerItem(
|
||||
title: 'Maintenance Center'.tr,
|
||||
icon: Icons.build,
|
||||
color: Colors.brown,
|
||||
onTap: () => Get.to(() => MaintainCenterPage())),
|
||||
DrawerItem(
|
||||
title: 'Health Insurance'.tr,
|
||||
icon: Icons.favorite,
|
||||
color: Colors.pink,
|
||||
onTap: () => Get.to(() => AssuranceHealthPage())),
|
||||
// DrawerItem(
|
||||
// title: 'Maintenance Center'.tr,
|
||||
// icon: Icons.build,
|
||||
// color: Colors.brown,
|
||||
// onTap: () => Get.to(() => MaintainCenterPage())),
|
||||
// DrawerItem(
|
||||
// title: 'Health Insurance'.tr,
|
||||
// icon: Icons.favorite,
|
||||
// color: Colors.pink,
|
||||
// onTap: () => Get.to(() => AssuranceHealthPage())),
|
||||
DrawerItem(
|
||||
title: 'Contact Us'.tr,
|
||||
icon: Icons.email,
|
||||
@@ -111,6 +114,12 @@ class AppDrawer extends StatelessWidget {
|
||||
icon: Icons.memory,
|
||||
color: Colors.greenAccent,
|
||||
onTap: () => Get.to(() => DeviceCompatibilityPage())),
|
||||
DrawerItem(
|
||||
title: 'Privacy Policy'.tr,
|
||||
icon: Icons.memory,
|
||||
color: Colors.greenAccent,
|
||||
onTap: () =>
|
||||
launchUrl(Uri.parse('${AppLink.server}/privacy_policy.php'))),
|
||||
DrawerItem(
|
||||
title: 'Settings'.tr,
|
||||
icon: Icons.settings,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
@@ -26,18 +27,19 @@ import 'widget/connect.dart';
|
||||
import 'widget/left_menu_map_captain.dart';
|
||||
import '../../../../main.dart';
|
||||
|
||||
// الويدجت الرئيسية للصفحة بعد تنظيمها
|
||||
// ==================================================================
|
||||
// Redesigned Main Widget (V3)
|
||||
// ==================================================================
|
||||
class HomeCaptain extends StatelessWidget {
|
||||
HomeCaptain({super.key});
|
||||
|
||||
// تم الإبقاء على تعريف الـ Controllers كما هو في الكود الأصلي
|
||||
final LocationController locationController = Get.put(LocationController());
|
||||
final HomeCaptainController homeCaptainController =
|
||||
Get.put(HomeCaptainController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// لم يتم تغيير أي شيء في هذه الأوامر
|
||||
// Initial calls remain the same.
|
||||
Get.put(HomeCaptainController());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
closeOverlayIfFound();
|
||||
@@ -46,19 +48,20 @@ class HomeCaptain extends StatelessWidget {
|
||||
showDriverGiftClaim(context);
|
||||
});
|
||||
|
||||
// التصميم الجديد: أصبح الـ build الرئيسي نظيفاً وواضحاً جداً
|
||||
// The stack is now even simpler.
|
||||
return Scaffold(
|
||||
appBar: const _HomeAppBar(), // 1. تم فصل الـ AppBar في ويدجت خاصة
|
||||
appBar: const _HomeAppBar(),
|
||||
drawer: AppDrawer(),
|
||||
body: Stack(
|
||||
children: [
|
||||
// كل جزء من الواجهة أصبح ويدجت منفصلة
|
||||
const _MapView(), // 2. تم فصل الخريطة
|
||||
const _DriverStatsOverlay(), // 3. تم فصل كارت الإحصائيات العلوي
|
||||
const _DriverDurationOverlay(), // 4. تم فصل كارت مدة العمل
|
||||
const _FloatingActionButtons(), // 5. تم فصل الأزرار الجانبية العائمة
|
||||
const _ConnectButtonOverlay(), // 6. تم فصل زر الاتصال السفلي
|
||||
leftMainMenuCaptainIcons(), // هذه بقيت كما هي
|
||||
// 1. The Map View is the base layer.
|
||||
const _MapView(),
|
||||
|
||||
// 2. The new floating "Status Pod" at the bottom.
|
||||
const _StatusPodOverlay(),
|
||||
|
||||
// This widget from the original code remains.
|
||||
leftMainMenuCaptainIcons(),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -66,64 +69,51 @@ class HomeCaptain extends StatelessWidget {
|
||||
}
|
||||
|
||||
// ==================================================================
|
||||
// الأجزاء الصغيرة التي تم فصلها (Helper Widgets)
|
||||
// هذه الويدجتس تحتوي على نفس كود التصميم الأصلي الخاص بك تماماً، ولكنها منظمة بشكل أفضل
|
||||
// Redesigned Helper Widgets (V3)
|
||||
// ==================================================================
|
||||
|
||||
/// 1. ويدجت الـ AppBar
|
||||
/// 1. The AppBar now contains the map actions in a PopupMenuButton.
|
||||
class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
const _HomeAppBar();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// نفس الكود الأصلي للـ AppBar
|
||||
final homeCaptainController = Get.find<HomeCaptainController>();
|
||||
return AppBar(
|
||||
elevation: 2,
|
||||
flexibleSpace: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.white, Colors.grey.shade50],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 4,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 1,
|
||||
shadowColor: Colors.black.withOpacity(0.1),
|
||||
title: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
'assets/images/logo.gif',
|
||||
height: 32,
|
||||
width: 35,
|
||||
height: 35,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
AppInformation.appName.split(' ')[0].toString().tr,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.blueColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
// Refuse count indicator
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: MyCircleContainer(
|
||||
child: Text(
|
||||
homeCaptainController.countRefuse.toString(),
|
||||
style: AppStyle.title,
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Center(
|
||||
child: MyCircleContainer(
|
||||
child: Text(
|
||||
homeCaptainController.countRefuse.toString(),
|
||||
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// The new PopupMenuButton for all map and ride actions.
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
@@ -165,28 +155,45 @@ class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
PopupMenuItem<String> _buildPopupMenuItem({
|
||||
required String value,
|
||||
IconData? icon,
|
||||
Widget? iconWidget,
|
||||
required String text,
|
||||
Color? iconColor,
|
||||
}) {
|
||||
return PopupMenuItem<String>(
|
||||
value: value,
|
||||
child: Row(
|
||||
children: [
|
||||
iconWidget ?? Icon(icon, color: iconColor ?? Colors.grey.shade600),
|
||||
const SizedBox(width: 16),
|
||||
Text(text),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
|
||||
/// 2. ويدجت الخريطة
|
||||
/// 2. The Map View is unchanged functionally.
|
||||
class _MapView extends StatelessWidget {
|
||||
const _MapView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final locationController = Get.find<LocationController>();
|
||||
// نفس الكود الأصلي للخريطة
|
||||
return GetBuilder<HomeCaptainController>(builder: (controller) {
|
||||
return controller.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: GoogleMap(
|
||||
padding: EdgeInsets.only(bottom: 50, top: 300),
|
||||
padding: const EdgeInsets.only(bottom: 110, top: 300),
|
||||
fortyFiveDegreeImageryEnabled: true,
|
||||
onMapCreated: controller.onMapCreated,
|
||||
minMaxZoomPreference: const MinMaxZoomPreference(6, 18),
|
||||
@@ -217,7 +224,7 @@ class _MapView extends StatelessWidget {
|
||||
myLocationEnabled: false,
|
||||
trafficEnabled: controller.mapTrafficON,
|
||||
buildingsEnabled: true,
|
||||
mapToolbarEnabled: true,
|
||||
mapToolbarEnabled: false,
|
||||
compassEnabled: true,
|
||||
zoomControlsEnabled: false,
|
||||
);
|
||||
@@ -225,353 +232,213 @@ class _MapView extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 3. ويدجت كارت الإحصائيات العلوي
|
||||
class _DriverStatsOverlay extends StatelessWidget {
|
||||
const _DriverStatsOverlay();
|
||||
/// 3. The floating "Status Pod" at the bottom of the screen.
|
||||
class _StatusPodOverlay extends StatelessWidget {
|
||||
const _StatusPodOverlay();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// نفس الكود الأصلي لكارت الإحصائيات
|
||||
return Positioned(
|
||||
top: 5,
|
||||
right: Get.width * .05,
|
||||
left: Get.width * .05,
|
||||
child: GetBuilder<HomeCaptainController>(
|
||||
builder: (homeCaptainController) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Colors.white, Colors.white70],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
width: Get.width * .8,
|
||||
height: 120,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.greenColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Entypo.wallet,
|
||||
color: AppColor.greenColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${"Today".tr}: ${(homeCaptainController.totalMoneyToday)}',
|
||||
style: AppStyle.title.copyWith(
|
||||
color: AppColor.greenColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.yellowColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Entypo.wallet,
|
||||
color: AppColor.yellowColor,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${AppInformation.appName}: ${(homeCaptainController.totalMoneyInSEFER)}',
|
||||
style: AppStyle.title.copyWith(
|
||||
color: AppColor.yellowColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${'Total Points is'.tr}: ${(homeCaptainController.totalPoints)}',
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: int.parse(
|
||||
(homeCaptainController.countRideToday)) <
|
||||
5
|
||||
? AppColor.accentColor
|
||||
: int.parse((homeCaptainController
|
||||
.countRideToday)) >
|
||||
5 &&
|
||||
int.parse((homeCaptainController
|
||||
.countRideToday)) <
|
||||
10
|
||||
? AppColor.yellowColor
|
||||
: AppColor.greenColor,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.directions_car_rounded,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${"Ride Today : ".tr}: ${(homeCaptainController.countRideToday)}',
|
||||
style: AppStyle.title.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
);
|
||||
},
|
||||
),
|
||||
void _showDetailsDialog(BuildContext context) {
|
||||
Get.dialog(
|
||||
const _DriverDetailsDialog(),
|
||||
barrierColor: Colors.black.withOpacity(0.3),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 4. ويدجت كارت مدة العمل
|
||||
class _DriverDurationOverlay extends StatelessWidget {
|
||||
const _DriverDurationOverlay();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// نفس الكود الأصلي لكارت المدة
|
||||
final homeCaptainController = Get.find<HomeCaptainController>();
|
||||
return Positioned(
|
||||
bottom: 65,
|
||||
right: Get.width * .1,
|
||||
left: Get.width * .1,
|
||||
child: GetBuilder<HomeCaptainController>(
|
||||
builder: (homeCaptainController) => Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.timer_outlined, color: AppColor.greenColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Active Duration:'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
(homeCaptainController.stringActiveDuration),
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.greenColor,
|
||||
),
|
||||
),
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: GestureDetector(
|
||||
onTap: () => _showDetailsDialog(context),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.85),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.5)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
spreadRadius: -5,
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.access_time, color: AppColor.accentColor),
|
||||
const ConnectWidget(),
|
||||
const Spacer(),
|
||||
_buildQuickStat(
|
||||
icon: Icons.directions_car_rounded,
|
||||
value: homeCaptainController.countRideToday,
|
||||
label: 'Rides'.tr,
|
||||
color: AppColor.blueColor,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildQuickStat(
|
||||
icon: Entypo.wallet,
|
||||
value: homeCaptainController.totalMoneyToday.toString(),
|
||||
label: 'Today'.tr,
|
||||
color: AppColor.greenColor,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Total Connection Duration:'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
(homeCaptainController.totalDurationToday),
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.accentColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 5. ويدجت الأزرار الجانبية العائمة
|
||||
class _FloatingActionButtons extends StatelessWidget {
|
||||
const _FloatingActionButtons();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// نفس الكود الأصلي للأزرار
|
||||
return Positioned(
|
||||
bottom: Get.height * .2,
|
||||
right: 6,
|
||||
child:
|
||||
GetBuilder<HomeCaptainController>(builder: (homeCaptainController) {
|
||||
return Column(
|
||||
Widget _buildQuickStat(
|
||||
{required IconData icon,
|
||||
required String value,
|
||||
required String label,
|
||||
required Color color}) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Platform.isAndroid
|
||||
? AnimatedContainer(
|
||||
duration: const Duration(microseconds: 200),
|
||||
width: homeCaptainController.widthMapTypeAndTraffic,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColor.blueColor),
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
Bubble().startBubbleHead(sendAppToBackground: true);
|
||||
},
|
||||
icon: Image.asset(
|
||||
'assets/images/logo1.png',
|
||||
fit: BoxFit.cover,
|
||||
width: 35,
|
||||
height: 35,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(microseconds: 200),
|
||||
width: homeCaptainController.widthMapTypeAndTraffic,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColor.blueColor),
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
Get.to(() => const AvailableRidesPage());
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.train_sharp,
|
||||
size: 29,
|
||||
color: AppColor.blueColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
box.read(BoxName.rideStatus) == 'Applied' ||
|
||||
box.read(BoxName.rideStatus) == 'Begin'
|
||||
? Positioned(
|
||||
bottom: Get.height * .2,
|
||||
right: 6,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(microseconds: 200),
|
||||
width: homeCaptainController.widthMapTypeAndTraffic,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColor.blueColor),
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
box.write(BoxName.rideStatus, 'delete');
|
||||
homeCaptainController.update();
|
||||
},
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
box.read(BoxName.rideStatus) == 'Applied'
|
||||
? {
|
||||
Get.to(() => PassengerLocationMapPage(),
|
||||
arguments:
|
||||
box.read(BoxName.rideArguments)),
|
||||
Get.put(MapDriverController())
|
||||
.changeRideToBeginToPassenger()
|
||||
}
|
||||
: {
|
||||
Get.to(() => PassengerLocationMapPage(),
|
||||
arguments:
|
||||
box.read(BoxName.rideArguments)),
|
||||
Get.put(MapDriverController())
|
||||
.startRideFromStartApp()
|
||||
};
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.directions_rounded,
|
||||
size: 29,
|
||||
color: AppColor.blueColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox()
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(width: 4),
|
||||
Text(value,
|
||||
style: AppStyle.title
|
||||
.copyWith(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
Text(label,
|
||||
style: AppStyle.title
|
||||
.copyWith(fontSize: 12, color: Colors.grey.shade700)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 6. ويدجت زر الاتصال السفلي
|
||||
class _ConnectButtonOverlay extends StatelessWidget {
|
||||
const _ConnectButtonOverlay();
|
||||
/// 4. The Dialog that shows detailed driver stats.
|
||||
class _DriverDetailsDialog extends StatelessWidget {
|
||||
const _DriverDetailsDialog();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// نفس الكود الأصلي لزر الاتصال
|
||||
return Positioned(
|
||||
bottom: 10,
|
||||
right: Get.width * .1,
|
||||
left: Get.width * .1,
|
||||
child: const ConnectWidget());
|
||||
final homeCaptainController = Get.find<HomeCaptainController>();
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||
child: AlertDialog(
|
||||
backgroundColor: Colors.white.withOpacity(0.95),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
titlePadding: const EdgeInsets.only(top: 20),
|
||||
title: Center(
|
||||
child: Text(
|
||||
'Your Activity'.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Divider(height: 20),
|
||||
_buildStatRow(
|
||||
icon: Entypo.wallet,
|
||||
color: AppColor.greenColor,
|
||||
label: 'Today'.tr,
|
||||
value: homeCaptainController.totalMoneyToday.toString(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatRow(
|
||||
icon: Entypo.wallet,
|
||||
color: AppColor.yellowColor,
|
||||
label: AppInformation.appName,
|
||||
value: homeCaptainController.totalMoneyInSEFER.toString(),
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildDurationRow(
|
||||
icon: Icons.timer_outlined,
|
||||
label: 'Active Duration:'.tr,
|
||||
value: homeCaptainController.stringActiveDuration,
|
||||
color: AppColor.greenColor,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildDurationRow(
|
||||
icon: Icons.access_time,
|
||||
label: 'Total Connection Duration:'.tr,
|
||||
value: homeCaptainController.totalDurationToday,
|
||||
color: AppColor.accentColor,
|
||||
),
|
||||
const Divider(height: 24),
|
||||
_buildStatRow(
|
||||
icon: Icons.star_border_rounded,
|
||||
color: AppColor.blueColor,
|
||||
label: 'Total Points'.tr,
|
||||
value: homeCaptainController.totalPoints.toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text('Close'.tr,
|
||||
style: AppStyle.title.copyWith(
|
||||
color: AppColor.blueColor, fontWeight: FontWeight.bold)),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatRow(
|
||||
{required IconData icon,
|
||||
required Color color,
|
||||
required String label,
|
||||
required String value}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 22),
|
||||
const SizedBox(width: 12),
|
||||
Text('$label:', style: AppStyle.title),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: AppStyle.title.copyWith(
|
||||
color: color, fontWeight: FontWeight.bold, fontSize: 18),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDurationRow(
|
||||
{required IconData icon,
|
||||
required String label,
|
||||
required String value,
|
||||
required Color color}) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(label, style: AppStyle.title),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.bold, color: color, fontSize: 16),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// هذه الويدجت المساعدة بقيت كما هي
|
||||
class _MapControlButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
@@ -606,7 +473,44 @@ class _MapControlButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// الدوال المساعدة الأخرى تبقى كما هي في ملفك...
|
||||
// showFirstTimeOfferNotification(BuildContext context) async { ... }
|
||||
// bool _checkIfFirstTime() { ... }
|
||||
// void _markAsNotFirstTime() { ... }
|
||||
/// NOTE: The _FloatingActionButtons and _MapControlButton widgets have been removed
|
||||
/// as their functionality is now integrated into the _HomeAppBar.
|
||||
///
|
||||
/// You will still need to modify your existing `ConnectWidget`
|
||||
/// to accept an `isCompact` boolean flag as mentioned in the previous design.
|
||||
/*
|
||||
class ConnectWidget extends StatelessWidget {
|
||||
final bool isCompact;
|
||||
const ConnectWidget({super.key, this.isCompact = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// ... your existing controller logic
|
||||
|
||||
if (isCompact) {
|
||||
// Return a smaller version for the pod
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: controller.isConnect ? AppColor.greenColor : AppColor.accentColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(controller.isConnect ? Icons.wifi_tethering_rounded : Icons.wifi_tethering_off_rounded, color: Colors.white, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
controller.isConnect ? 'Online'.tr : 'Offline'.tr,
|
||||
style: AppStyle.title.copyWith(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Return the original, larger button
|
||||
return ElevatedButton.icon(...)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:sefer_driver/constant/box_name.dart';
|
||||
import 'package:sefer_driver/controller/firebase/local_notification.dart';
|
||||
import 'package:sefer_driver/main.dart';
|
||||
import 'package:sefer_driver/views/auth/captin/login_captin.dart';
|
||||
import 'package:sefer_driver/views/home/Captin/driver_map_page.dart';
|
||||
import 'package:sefer_driver/views/home/Captin/orderCaptin/vip_order_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -14,6 +15,7 @@ import '../../../../../constant/links.dart';
|
||||
import '../../../../../controller/firebase/firbase_messge.dart';
|
||||
import '../../../../../controller/functions/crud.dart';
|
||||
import '../../../../../controller/home/captin/order_request_controller.dart';
|
||||
import '../../../../../controller/home/navigation/navigation_view.dart';
|
||||
import '../../../../Rate/ride_calculate_driver.dart';
|
||||
import '../../../../auth/syria/registration_view.dart';
|
||||
|
||||
@@ -156,18 +158,42 @@ GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
|
||||
child: Builder(builder: (context) {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
Get.to(() => const RegistrationView());
|
||||
box.remove(BoxName.agreeTerms);
|
||||
Get.to(() => const NavigationView());
|
||||
|
||||
// box.write(BoxName.statusDriverLocation, 'off');
|
||||
},
|
||||
icon: const Icon(
|
||||
FontAwesome5.grin_tears,
|
||||
FontAwesome5.map,
|
||||
size: 29,
|
||||
color: AppColor.blueColor,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
// AnimatedContainer(
|
||||
// duration: const Duration(microseconds: 200),
|
||||
// width: controller.widthMapTypeAndTraffic,
|
||||
// decoration: BoxDecoration(
|
||||
// color: AppColor.secondaryColor,
|
||||
// border: Border.all(color: AppColor.blueColor),
|
||||
// borderRadius: BorderRadius.circular(15)),
|
||||
// child: Builder(builder: (context) {
|
||||
// return IconButton(
|
||||
// onPressed: () async {
|
||||
// box.remove(BoxName.agreeTerms);
|
||||
// Get.to(() => const NavigationView());
|
||||
|
||||
// // box.write(BoxName.statusDriverLocation, 'off');
|
||||
// },
|
||||
// icon: const Icon(
|
||||
// FontAwesome5.grin_tears,
|
||||
// size: 29,
|
||||
// color: AppColor.blueColor,
|
||||
// ),
|
||||
// );
|
||||
// }),
|
||||
// ),
|
||||
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
|
||||
@@ -204,7 +204,7 @@ GetBuilder<MapDriverController> speedCircle() {
|
||||
? Positioned(
|
||||
// New: تم وضع دائرة السرعة في الأسفل يمينًا
|
||||
bottom: 25,
|
||||
left: 16,
|
||||
left: 3,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
|
||||
@@ -1,117 +1,3 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
// import '../../../../controller/functions/location_controller.dart';
|
||||
// import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||
|
||||
// class GoogleDriverMap extends StatelessWidget {
|
||||
// const GoogleDriverMap({
|
||||
// super.key,
|
||||
// required this.locationController,
|
||||
// });
|
||||
|
||||
// final LocationController locationController;
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// Get.put(MapDriverController());
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.all(8.0),
|
||||
// child: GetBuilder<MapDriverController>(
|
||||
// builder: (controller) => Column(
|
||||
// children: [
|
||||
// SizedBox(
|
||||
// height: Get.height * .92,
|
||||
// child: GoogleMap(
|
||||
// onMapCreated: controller.onMapCreated,
|
||||
// zoomControlsEnabled: true,
|
||||
// // initialCameraPosition: CameraPosition(
|
||||
// // target: locationController.myLocation,
|
||||
// // zoom: 13,
|
||||
// // bearing: locationController.heading,
|
||||
// // tilt: 40,
|
||||
// // ),
|
||||
// initialCameraPosition: CameraPosition(
|
||||
// target: locationController.myLocation,
|
||||
// zoom: 17,
|
||||
// bearing: locationController.heading, // استخدام اتجاه السائق
|
||||
// tilt: 60, // زاوية ميل
|
||||
// ),
|
||||
// cameraTargetBounds:
|
||||
// CameraTargetBounds.unbounded, // Allow unrestricted movement
|
||||
// onCameraMove: (position) {
|
||||
// CameraPosition(
|
||||
// target: locationController.myLocation,
|
||||
// zoom: 13,
|
||||
// bearing: locationController.heading,
|
||||
// tilt: 40,
|
||||
// );
|
||||
// //todo
|
||||
// // locationController.myLocation = position.target;
|
||||
// //
|
||||
// // controller.mapController
|
||||
// // ?.animateCamera(CameraUpdate.newCameraPosition(position));
|
||||
// },
|
||||
// minMaxZoomPreference: const MinMaxZoomPreference(8, 15),
|
||||
// myLocationEnabled: true,
|
||||
// myLocationButtonEnabled: true,
|
||||
// compassEnabled: true,
|
||||
// mapType: MapType.terrain,
|
||||
// rotateGesturesEnabled: true,
|
||||
// scrollGesturesEnabled: true,
|
||||
// trafficEnabled: false,
|
||||
// buildingsEnabled: true,
|
||||
// mapToolbarEnabled: true,
|
||||
// fortyFiveDegreeImageryEnabled: true,
|
||||
// zoomGesturesEnabled: true,
|
||||
// polylines: {
|
||||
// Polyline(
|
||||
// zIndex: 2,
|
||||
// geodesic: true,
|
||||
// polylineId: const PolylineId('route1'),
|
||||
// points: controller.polylineCoordinates,
|
||||
// color: const Color.fromARGB(255, 163, 81, 246),
|
||||
// width: 5,
|
||||
// ),
|
||||
// Polyline(
|
||||
// zIndex: 2,
|
||||
// geodesic: true,
|
||||
// polylineId: const PolylineId('route'),
|
||||
// points: controller.polylineCoordinatesDestination,
|
||||
// color: const Color.fromARGB(255, 10, 29, 126),
|
||||
// width: 5,
|
||||
// ),
|
||||
// },
|
||||
// markers: {
|
||||
// Marker(
|
||||
// markerId: MarkerId('MyLocation'.tr),
|
||||
// position: locationController.myLocation,
|
||||
// draggable: true,
|
||||
// icon: controller.carIcon,
|
||||
// rotation: locationController.heading,
|
||||
// ),
|
||||
// Marker(
|
||||
// markerId: MarkerId('start'.tr),
|
||||
// position: controller.latLngPassengerLocation,
|
||||
// draggable: true,
|
||||
// icon: controller.startIcon,
|
||||
// ),
|
||||
// Marker(
|
||||
// markerId: MarkerId('end'.tr),
|
||||
// position: controller.latLngPassengerDestination,
|
||||
// draggable: true,
|
||||
// icon: controller.endIcon,
|
||||
// ),
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
@@ -179,15 +65,30 @@ class GoogleDriverMap extends StatelessWidget {
|
||||
startCap: Cap.roundCap,
|
||||
endCap: Cap.roundCap,
|
||||
),
|
||||
Polyline(
|
||||
zIndex: 2,
|
||||
// Polyline(
|
||||
// zIndex: 2,
|
||||
|
||||
polylineId: const PolylineId('route'),
|
||||
points: controller.polylineCoordinatesDestination,
|
||||
color: const Color.fromARGB(255, 10, 29, 126),
|
||||
width: 6, // Changed: زيادة عرض الخط
|
||||
startCap: Cap.roundCap,
|
||||
endCap: Cap.roundCap,
|
||||
// polylineId: const PolylineId('route'),
|
||||
// points: controller.polylineCoordinatesDestination,
|
||||
// color: const Color.fromARGB(255, 10, 29, 126),
|
||||
// width: 6, // Changed: زيادة عرض الخط
|
||||
// startCap: Cap.roundCap,
|
||||
// endCap: Cap.roundCap,
|
||||
// ),
|
||||
Polyline(
|
||||
polylineId: const PolylineId('upcoming_route'),
|
||||
points: controller.upcomingPathPoints,
|
||||
color: Colors.blue, // أو أي لون آخر تختاره للمسار
|
||||
width: 8,
|
||||
zIndex: 2,
|
||||
),
|
||||
// 2. الخط المقطوع (تحت)
|
||||
Polyline(
|
||||
polylineId: const PolylineId('traveled_route'),
|
||||
points: controller.traveledPathPoints,
|
||||
color: Colors.grey.withOpacity(0.8),
|
||||
width: 7,
|
||||
zIndex: 1,
|
||||
),
|
||||
},
|
||||
markers: {
|
||||
|
||||
@@ -14,7 +14,7 @@ class GoogleMapApp extends StatelessWidget {
|
||||
return GetBuilder<MapDriverController>(
|
||||
builder: (mapDriverController) => mapDriverController.isRideStarted
|
||||
? Positioned(
|
||||
left: 150,
|
||||
right: 3,
|
||||
bottom: 20,
|
||||
child: Container(
|
||||
decoration: AppStyle.boxDecoration,
|
||||
|
||||
@@ -144,6 +144,11 @@ class PassengerInfoWindow extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
onPressed: () async {
|
||||
controller.getRoute(
|
||||
origin: controller.latLngPassengerLocation,
|
||||
destination: controller.latLngPassengerDestination,
|
||||
routeColor: Colors.blue // أو أي لون
|
||||
);
|
||||
if (await controller
|
||||
.calculateDistanceBetweenDriverAndPassengerLocation() <
|
||||
140) {
|
||||
|
||||
@@ -274,13 +274,18 @@ class _OrderRequestPageState extends State<OrderRequestPage> {
|
||||
controller.myList[8].toString(),
|
||||
controller.myList[9].toString(),
|
||||
];
|
||||
FirebaseMessagesController()
|
||||
.sendNotificationToPassengerToken(
|
||||
"Accepted Ride".tr,
|
||||
'your ride is Accepted'.tr,
|
||||
controller.myList[9].toString(),
|
||||
bodyToPassenger,
|
||||
'start.wav');
|
||||
final fmc =
|
||||
Get.isRegistered<FirebaseMessagesController>()
|
||||
? Get.find<FirebaseMessagesController>()
|
||||
: Get.put(FirebaseMessagesController());
|
||||
|
||||
fmc.sendNotificationToDriverMAP(
|
||||
"Accepted Ride".tr,
|
||||
'your ride is Accepted'.tr,
|
||||
controller.myList[9].toString(),
|
||||
bodyToPassenger,
|
||||
'start.wav',
|
||||
);
|
||||
Get.back();
|
||||
box.write(BoxName.rideArguments, {
|
||||
'passengerLocation':
|
||||
|
||||
@@ -95,7 +95,7 @@ class CardSeferWalletDriver extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'محفظة انطلق',
|
||||
'رصيد انطلق',
|
||||
style: AppStyle.headTitle.copyWith(
|
||||
fontFamily: 'Amiri', // خط يوحي بالفخامة
|
||||
color: Colors.white,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
import 'package:sefer_driver/constant/colors.dart';
|
||||
import 'package:sefer_driver/constant/style.dart';
|
||||
import 'package:sefer_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:sefer_driver/views/widgets/mycircular.dart';
|
||||
import 'package:sefer_driver/constant/style.dart'; // Assuming this has your text styles
|
||||
import 'package:sefer_driver/views/widgets/mycircular.dart'; // Assuming this is your loading widget
|
||||
|
||||
import '../../../controller/payment/driver_payment_controller.dart';
|
||||
|
||||
@@ -12,43 +12,133 @@ class PaymentHistoryDriverPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Initialize your controller
|
||||
Get.put(DriverWalletHistoryController());
|
||||
return MyScafolld(
|
||||
title: 'Payment History'.tr,
|
||||
body: [
|
||||
GetBuilder<DriverWalletHistoryController>(
|
||||
builder: (controller) => controller.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: ListView.builder(
|
||||
itemCount: controller.archive.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var list = controller.archive[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: double.parse(list['amount']) < 0
|
||||
? AppColor.redColor.withOpacity(.4)
|
||||
: AppColor.greenColor.withOpacity(.4)),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
list['amount'],
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
list['created_at'],
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Payment History'.tr),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 1,
|
||||
),
|
||||
backgroundColor: Colors.grey[100],
|
||||
body: GetBuilder<DriverWalletHistoryController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
// Using your custom loading indicator
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (controller.archive.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.account_balance_wallet_outlined,
|
||||
size: 80, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No transactions yet'.tr,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(color: Colors.grey[600]),
|
||||
),
|
||||
)
|
||||
],
|
||||
isleading: true);
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AnimationLimiter(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
itemCount: controller.archive.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var transaction = controller.archive[index];
|
||||
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: _TransactionCard(transaction: transaction),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A dedicated widget for displaying a single transaction with a modern UI.
|
||||
class _TransactionCard extends StatelessWidget {
|
||||
final Map<String, dynamic> transaction;
|
||||
|
||||
const _TransactionCard({required this.transaction});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Safely parse the amount to avoid errors
|
||||
final double amount =
|
||||
double.tryParse(transaction['amount']?.toString() ?? '0') ?? 0;
|
||||
|
||||
final bool isCredit = amount >= 0;
|
||||
|
||||
final Color indicatorColor =
|
||||
isCredit ? AppColor.greenColor : AppColor.redColor;
|
||||
final IconData iconData =
|
||||
isCredit ? Icons.arrow_upward_rounded : Icons.arrow_downward_rounded;
|
||||
final String transactionType = (isCredit ? 'Credit'.tr : 'Debit'.tr).tr;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withOpacity(0.05),
|
||||
margin: const EdgeInsets.only(bottom: 12.0),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
clipBehavior: Clip.antiAlias, // Ensures the color bar is clipped neatly
|
||||
child: IntrinsicHeight(
|
||||
// Ensures the color bar and content have the same height
|
||||
child: Row(
|
||||
children: [
|
||||
// Left-side color indicator bar
|
||||
Container(width: 6, color: indicatorColor),
|
||||
|
||||
Expanded(
|
||||
child: ListTile(
|
||||
leading: Icon(iconData, color: indicatorColor, size: 30),
|
||||
title: Text(
|
||||
// Use .abs() to remove the negative sign from the display
|
||||
'${amount.abs().toStringAsFixed(2)} ${'SYP'.tr}',
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
transaction['created_at'] ?? 'No date',
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
trailing: Text(
|
||||
transactionType,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontSize: 14,
|
||||
color: indicatorColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class WalletCaptainRefactored extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
captainWalletController.refreshCaptainWallet();
|
||||
return MyScafolld(
|
||||
title: 'Driver Wallet'.tr,
|
||||
title: 'Driver Balance'.tr,
|
||||
isleading: true,
|
||||
action: IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
@@ -173,7 +173,7 @@ class WalletCaptainRefactored extends StatelessWidget {
|
||||
'This amount for all trip I get from Passengers'
|
||||
.tr),
|
||||
child: const Icon(Icons.headphones)),
|
||||
'${'Total Amount:'.tr} ${controller.totalAmount} ${'S.P'.tr}',
|
||||
'${'Total Amount:'.tr} ${controller.totalAmount} ${'SYP'.tr}',
|
||||
'This amount for all trip I get from Passengers'.tr,
|
||||
duration: const Duration(seconds: 6),
|
||||
backgroundColor: AppColor.yellowColor,
|
||||
@@ -195,7 +195,7 @@ class WalletCaptainRefactored extends StatelessWidget {
|
||||
' Intaleq Wallet'.tr),
|
||||
child: const Icon(Icons.headphones),
|
||||
),
|
||||
'${'Total Amount:'.tr} ${controller.totalAmountVisa} ${'S.P'.tr}',
|
||||
'${'Total Amount:'.tr} ${controller.totalAmountVisa} ${'SYP'.tr}',
|
||||
'This amount for all trip I get from Passengers and Collected For me in'
|
||||
.tr +
|
||||
' ${AppInformation.appName} Wallet'.tr,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
|
||||
import 'package:sefer_driver/constant/colors.dart';
|
||||
import 'package:sefer_driver/constant/style.dart';
|
||||
import 'package:sefer_driver/views/widgets/my_scafold.dart';
|
||||
import 'package:sefer_driver/views/widgets/mycircular.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../controller/payment/driver_payment_controller.dart';
|
||||
|
||||
class WeeklyPaymentPage extends StatelessWidget {
|
||||
@@ -14,128 +14,210 @@ class WeeklyPaymentPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(DriverWalletHistoryController());
|
||||
return MyScafolld(
|
||||
title: 'Payment History'.tr,
|
||||
body: [
|
||||
GetBuilder<DriverWalletHistoryController>(
|
||||
builder: (controller) => controller.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: Get.width * .8,
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
controller.weeklyList.isEmpty
|
||||
? '0'
|
||||
: controller.weeklyList[0]
|
||||
['totalAmount']
|
||||
.toString(),
|
||||
style: AppStyle.number,
|
||||
),
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Weekly Summary'.tr),
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 1,
|
||||
),
|
||||
backgroundColor: Colors.grey[100],
|
||||
body: GetBuilder<DriverWalletHistoryController>(
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) {
|
||||
return const Center(child: MyCircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// 1. Prominent Summary Card at the top
|
||||
_buildSummaryCard(controller),
|
||||
|
||||
// 2. A title for the transactions list
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.list_alt, color: Colors.grey[600]),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Transactions this week'.tr,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 3. The animated list of transactions
|
||||
Expanded(
|
||||
child: controller.weeklyList.isEmpty
|
||||
? _buildEmptyState(context)
|
||||
: AnimationLimiter(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
itemCount: controller.weeklyList.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var transaction = controller.weeklyList[index];
|
||||
return AnimationConfiguration.staggeredList(
|
||||
position: index,
|
||||
duration: const Duration(milliseconds: 375),
|
||||
child: SlideAnimation(
|
||||
verticalOffset: 50.0,
|
||||
child: FadeInAnimation(
|
||||
child: _TransactionListItem(
|
||||
transaction: transaction),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
' Total weekly is '.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10, vertical: 5),
|
||||
child: SizedBox(
|
||||
height: Get.height * .75,
|
||||
child: controller.weeklyList.isNotEmpty
|
||||
? ListView.builder(
|
||||
itemCount: controller.weeklyList.length,
|
||||
itemBuilder:
|
||||
(BuildContext context, int index) {
|
||||
var list = controller.weeklyList[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
children: [
|
||||
Card(
|
||||
elevation: 2,
|
||||
color: list['paymentMethod'] ==
|
||||
'visa'
|
||||
? AppColor.blueColor
|
||||
: AppColor.secondaryColor,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
list['paymentMethod'] ==
|
||||
'Remainder'
|
||||
? 'Remainder'.tr
|
||||
: list['paymentMethod'] ==
|
||||
'fromBudget'
|
||||
? 'fromBudget'.tr
|
||||
: list[
|
||||
'paymentMethod'],
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.spaceBetween,
|
||||
children: [
|
||||
Card(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.all(
|
||||
8.0),
|
||||
child: Text(
|
||||
list['amount'],
|
||||
style: AppStyle.number,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat(
|
||||
'yyyy-MM-dd hh:mm a')
|
||||
.format(DateTime.parse(
|
||||
list[
|
||||
'dateUpdated'])),
|
||||
style: AppStyle.number,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// A widget for the top summary card.
|
||||
Widget _buildSummaryCard(DriverWalletHistoryController controller) {
|
||||
final String totalAmount = controller.weeklyList.isEmpty
|
||||
? '0.00'
|
||||
: controller.weeklyList[0]['totalAmount']?.toString() ?? '0.00';
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
elevation: 4,
|
||||
shadowColor: AppColor.primaryColor.withOpacity(0.2),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColor.primaryColor, AppColor.greenColor],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.account_balance_wallet,
|
||||
color: Colors.white, size: 40),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Total Weekly Earnings'.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(color: Colors.white70, fontSize: 16),
|
||||
),
|
||||
)
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$totalAmount ${'SYP'.tr}',
|
||||
style: AppStyle.number
|
||||
.copyWith(color: Colors.white, fontSize: 32),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// A dedicated widget for the list item.
|
||||
Widget _TransactionListItem({required Map<String, dynamic> transaction}) {
|
||||
final String paymentMethod = transaction['paymentMethod'] ?? 'Unknown';
|
||||
final String amount = transaction['amount']?.toString() ?? '0';
|
||||
final DateTime? date = DateTime.tryParse(transaction['dateUpdated'] ?? '');
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withOpacity(0.05),
|
||||
margin: const EdgeInsets.only(bottom: 12.0),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
child: Icon(_getPaymentIcon(paymentMethod),
|
||||
color: AppColor.primaryColor, size: 22),
|
||||
),
|
||||
title: Text(
|
||||
'$amount ${'SYP'.tr}',
|
||||
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text(
|
||||
date != null
|
||||
? DateFormat('EEEE, hh:mm a', Get.locale?.toString())
|
||||
.format(date) // e.g., Tuesday, 10:11 AM
|
||||
: 'Invalid Date',
|
||||
style: AppStyle.title.copyWith(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
trailing: Chip(
|
||||
label: Text(
|
||||
_getTranslatedPaymentMethod(paymentMethod),
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
side: BorderSide.none,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.receipt_long_outlined, size: 80, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No transactions this week'.tr,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.headlineSmall
|
||||
?.copyWith(color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
isleading: true);
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to get a specific icon for each payment method.
|
||||
IconData _getPaymentIcon(String paymentMethod) {
|
||||
switch (paymentMethod.toLowerCase()) {
|
||||
case 'visa':
|
||||
return Icons.credit_card;
|
||||
case 'frombudget':
|
||||
return Icons.account_balance_wallet_outlined;
|
||||
case 'remainder':
|
||||
return Icons.receipt_long;
|
||||
case 'cash':
|
||||
return Icons.money;
|
||||
default:
|
||||
return Icons.payment;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get translated or formatted payment method names.
|
||||
String _getTranslatedPaymentMethod(String paymentMethod) {
|
||||
switch (paymentMethod) {
|
||||
case 'Remainder':
|
||||
return 'Remainder'.tr;
|
||||
case 'fromBudget':
|
||||
return 'From Budget'.tr;
|
||||
default:
|
||||
return paymentMethod;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
||||
Reference in New Issue
Block a user