first commit
This commit is contained in:
34
siro_driver/lib/controller/auth/apple_sigin.dart
Executable file
34
siro_driver/lib/controller/auth/apple_sigin.dart
Executable file
@@ -0,0 +1,34 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
||||
|
||||
class AuthController extends GetxController {
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
|
||||
Future<User?> signInWithApple() async {
|
||||
try {
|
||||
final appleCredential = await SignInWithApple.getAppleIDCredential(
|
||||
scopes: [
|
||||
AppleIDAuthorizationScopes.email,
|
||||
AppleIDAuthorizationScopes.fullName,
|
||||
],
|
||||
);
|
||||
|
||||
final oAuthProvider = OAuthProvider('apple.com');
|
||||
final credential = oAuthProvider.credential(
|
||||
idToken: appleCredential.identityToken,
|
||||
accessToken: appleCredential.authorizationCode,
|
||||
);
|
||||
|
||||
UserCredential userCredential =
|
||||
await _auth.signInWithCredential(credential);
|
||||
return userCredential.user;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void signOut() async {
|
||||
await _auth.signOut();
|
||||
}
|
||||
}
|
||||
60
siro_driver/lib/controller/auth/captin/history_captain.dart
Executable file
60
siro_driver/lib/controller/auth/captin/history_captain.dart
Executable file
@@ -0,0 +1,60 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../views/home/Captin/history/history_details_page.dart';
|
||||
import '../../functions/crud.dart';
|
||||
|
||||
class HistoryCaptainController extends GetxController {
|
||||
bool isloading = false;
|
||||
Map historyData = {};
|
||||
Map historyDetailsData = {};
|
||||
late String orderID;
|
||||
getOrderId(String orderId) {
|
||||
orderID = orderId;
|
||||
update();
|
||||
}
|
||||
|
||||
getHistory() async {
|
||||
isloading = true;
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getDriverOrder,
|
||||
payload: {'driver_id': box.read(BoxName.driverID)});
|
||||
if (res != 'failure') {
|
||||
historyData = jsonDecode(res);
|
||||
isloading = false;
|
||||
update();
|
||||
} else {
|
||||
Get.defaultDialog(
|
||||
title: 'No ride yet'.tr,
|
||||
middleText: '',
|
||||
barrierDismissible: false,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Back'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
getHistoryDetails(String orderId) async {
|
||||
isloading = true;
|
||||
var res = await CRUD()
|
||||
.get(link: AppLink.getRideOrderID, payload: {'id': (orderId)});
|
||||
historyDetailsData = jsonDecode(res);
|
||||
isloading = false;
|
||||
update();
|
||||
Get.to(() => HistoryDetailsPage());
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
getHistory();
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
609
siro_driver/lib/controller/auth/captin/invit_controller.dart
Executable file
609
siro_driver/lib/controller/auth/captin/invit_controller.dart
Executable file
@@ -0,0 +1,609 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/contact.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/controller/home/payment/captain_wallet_controller.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../firebase/local_notification.dart';
|
||||
import '../../functions/launch.dart';
|
||||
import '../../notification/notification_captain_controller.dart';
|
||||
|
||||
class InviteController extends GetxController {
|
||||
final TextEditingController invitePhoneController = TextEditingController();
|
||||
List driverInvitationData = [];
|
||||
List driverInvitationDataToPassengers = [];
|
||||
String? couponCode;
|
||||
String? driverCouponCode;
|
||||
|
||||
// **FIX**: Added the missing 'contacts' and 'contactMaps' definitions.
|
||||
List<Contact> contacts = [];
|
||||
RxList<Map<String, dynamic>> contactMaps = <Map<String, dynamic>>[].obs;
|
||||
|
||||
int selectedTab = 0;
|
||||
PassengerStats passengerStats = PassengerStats();
|
||||
void updateSelectedTab(int index) {
|
||||
selectedTab = index;
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> shareDriverCode() async {
|
||||
if (driverCouponCode != null) {
|
||||
final String shareText =
|
||||
'''Join Intaleq as a driver using my referral code!
|
||||
Use code: $driverCouponCode
|
||||
Download the Intaleq Driver app now and earn rewards!
|
||||
''';
|
||||
await Share.share(shareText);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sharePassengerCode() async {
|
||||
if (couponCode != null) {
|
||||
final String shareText = '''Get a discount on your first Intaleq ride!
|
||||
Use my referral code: $couponCode
|
||||
Download the Intaleq app now and enjoy your ride!
|
||||
''';
|
||||
await Share.share(shareText);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// **MODIFIED**: Sync contacts automatically on controller initialization.
|
||||
syncContactsToServerOnce();
|
||||
// fetchDriverStats();
|
||||
}
|
||||
|
||||
// --- NEW LOGIC: ONE-TIME CONTACTS SYNC ---
|
||||
|
||||
/// **NEW**: Syncs all phone contacts to the server, but only runs once per user.
|
||||
Future<void> syncContactsToServerOnce() async {
|
||||
final String syncFlagKey = 'contactsSynced_${box.read(BoxName.driverID)}';
|
||||
|
||||
// 1. Check if contacts have already been synced for this user.
|
||||
if (box.read(syncFlagKey) == true) {
|
||||
print("Contacts have already been synced for this user.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. Request permission and fetch all contacts.
|
||||
if (await FlutterContacts.requestPermission(readonly: true)) {
|
||||
// mySnackbarSuccess('Starting contacts sync in background...'.tr);
|
||||
final List<Contact> allContacts =
|
||||
await FlutterContacts.getContacts(withProperties: true);
|
||||
// **FIX**: Assign fetched contacts to the class variable.
|
||||
contacts = allContacts;
|
||||
contactMaps.value = contacts.map((contact) {
|
||||
return {
|
||||
'name': contact.displayName,
|
||||
'phones':
|
||||
contact.phones.map((phone) => phone.normalizedNumber).toList(),
|
||||
'emails': contact.emails.map((email) => email.address).toList(),
|
||||
};
|
||||
}).toList();
|
||||
update();
|
||||
|
||||
// 3. Loop through contacts and save them to the server.
|
||||
for (var contact in allContacts) {
|
||||
if (contact.phones.isNotEmpty) {
|
||||
// Use the normalized phone number for consistency.
|
||||
var phone = contact.phones.first.normalizedNumber;
|
||||
if (phone.isNotEmpty) {
|
||||
CRUD().post(link: AppLink.savePhonesSyria, payload: {
|
||||
"driverId": box.read(BoxName.driverID), // Associate with driver
|
||||
"name": contact.displayName ?? 'No Name',
|
||||
"phone": phone,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. After a successful sync, set the flag to prevent future syncs.
|
||||
await box.write(syncFlagKey, true);
|
||||
// mySnackbarSuccess('Contacts sync completed successfully!'.tr);
|
||||
}
|
||||
} catch (e) {
|
||||
// mySnackeBarError('An error occurred during contact sync: $e'.tr);
|
||||
}
|
||||
}
|
||||
|
||||
// --- NEW LOGIC: NATIVE CONTACT PICKER ---
|
||||
|
||||
/// **MODIFIED**: This function now opens the phone's native contact picker.
|
||||
Future<void> pickContactFromNativeApp() async {
|
||||
try {
|
||||
log('=== START: FETCHING ALL CONTACTS FOR BOTTOM SHEET ===',
|
||||
name: 'ContactPicker');
|
||||
|
||||
if (await FlutterContacts.requestPermission(readonly: true)) {
|
||||
// عرض شاشة تحميل بسيطة ريثما يتم جلب الأسماء
|
||||
Get.dialog(const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false);
|
||||
|
||||
log('Permission granted. Calling FlutterContacts.getContacts()...',
|
||||
name: 'ContactPicker');
|
||||
|
||||
// جلب جميع جهات الاتصال إجبارياً من الصفر مع خصائصها
|
||||
List<Contact> allContacts =
|
||||
await FlutterContacts.getContacts(withProperties: true);
|
||||
|
||||
log('Total Contacts Fetched from Device: ${allContacts.length}',
|
||||
name: 'ContactPicker');
|
||||
|
||||
// فصل الأسماء لمعرفة الخلل
|
||||
List<Contact> validContacts = [];
|
||||
List<Contact> invalidContacts = [];
|
||||
|
||||
for (var c in allContacts) {
|
||||
if (c.phones.isNotEmpty) {
|
||||
validContacts.add(c);
|
||||
} else {
|
||||
invalidContacts.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
log('Contacts WITH phone numbers: ${validContacts.length}',
|
||||
name: 'ContactPicker');
|
||||
log('Contacts WITHOUT phone numbers: ${invalidContacts.length}',
|
||||
name: 'ContactPicker');
|
||||
|
||||
// طباعة أول 20 اسم صالح
|
||||
log('--- Sample of VALID Contacts ---', name: 'ContactPicker');
|
||||
for (int i = 0; i < validContacts.length && i < 20; i++) {
|
||||
log('[$i] Name: ${validContacts[i].displayName}, Phone: ${validContacts[i].phones.first.number}',
|
||||
name: 'ContactPicker');
|
||||
}
|
||||
|
||||
// طباعة أول 20 اسم غير صالح (بدون أرقام) لفحص المشكلة
|
||||
log('--- Sample of INVALID Contacts (No Phone) ---',
|
||||
name: 'ContactPicker');
|
||||
for (int i = 0; i < invalidContacts.length && i < 20; i++) {
|
||||
log('[$i] Name: ${invalidContacts[i].displayName}',
|
||||
name: 'ContactPicker');
|
||||
}
|
||||
|
||||
Get.back(); // إغلاق شاشة التحميل
|
||||
|
||||
if (validContacts.isEmpty) {
|
||||
mySnackeBarError('No contacts with phone numbers found'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
// متغيرات للبحث داخل القائمة المنسدلة
|
||||
RxList<Contact> filteredContacts = validContacts.obs;
|
||||
TextEditingController searchController = TextEditingController();
|
||||
|
||||
// دالة لتنظيف النصوص من أي رموز معطوبة
|
||||
String sanitizeText(String input) {
|
||||
if (input.isEmpty) return '';
|
||||
return input
|
||||
.replaceAll(
|
||||
RegExp(r'[^\x00-\x7F\u0600-\u06FF\u08A0-\u08FF\p{L}\p{N}\s]'),
|
||||
'')
|
||||
.trim();
|
||||
}
|
||||
|
||||
// فتح دليل هاتف مخصص داخل التطبيق
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
height: Get.height * 0.85,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(Get.context!).scaffoldBackgroundColor,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 10, bottom: 10),
|
||||
width: 50,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[400],
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
Text("Select a Contact".tr,
|
||||
style: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: TextField(
|
||||
controller: searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search name or number...".tr,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 0),
|
||||
),
|
||||
onChanged: (value) {
|
||||
filteredContacts.value = validContacts.where((c) {
|
||||
final nameMatch = c.displayName
|
||||
.toLowerCase()
|
||||
.contains(value.toLowerCase());
|
||||
final phoneMatch =
|
||||
c.phones.first.number.contains(value);
|
||||
return nameMatch || phoneMatch;
|
||||
}).toList();
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Obx(() => ListView.builder(
|
||||
itemCount: filteredContacts.length,
|
||||
itemBuilder: (context, index) {
|
||||
Contact c = filteredContacts[index];
|
||||
var firstPhone = c.phones.first;
|
||||
|
||||
String selectedPhone =
|
||||
firstPhone.normalizedNumber.isNotEmpty
|
||||
? firstPhone.normalizedNumber
|
||||
: firstPhone.number;
|
||||
|
||||
String safeName = sanitizeText(c.displayName);
|
||||
if (safeName.isEmpty) safeName = 'Unknown'.tr;
|
||||
|
||||
String safePhone = sanitizeText(selectedPhone);
|
||||
String initial = safeName.isNotEmpty
|
||||
? safeName[0].toUpperCase()
|
||||
: '?';
|
||||
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor:
|
||||
Colors.blueAccent.withOpacity(0.1),
|
||||
child: Text(initial,
|
||||
style: const TextStyle(
|
||||
color: Colors.blueAccent)),
|
||||
),
|
||||
title: Text(safeName),
|
||||
subtitle: Text(safePhone,
|
||||
textDirection: TextDirection.ltr),
|
||||
onTap: () {
|
||||
selectPhone(selectedPhone);
|
||||
},
|
||||
);
|
||||
},
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
isScrollControlled: true,
|
||||
);
|
||||
} else {
|
||||
log('Permission DENIED', name: 'ContactPicker');
|
||||
mySnackeBarError('Contact permission is required to pick contacts'.tr);
|
||||
}
|
||||
} catch (e) {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
log('CRITICAL ERROR: $e', name: 'ContactPicker');
|
||||
mySnackeBarError('An error occurred while loading contacts: $e'.tr);
|
||||
}
|
||||
log('=== END: FETCHING CONTACTS ===', name: 'ContactPicker');
|
||||
}
|
||||
|
||||
/// **FIX**: Added the missing 'selectPhone' method.
|
||||
void selectPhone(String phone) {
|
||||
// Format the selected phone number and update the text field.
|
||||
invitePhoneController.text = _formatSyrianPhoneNumber(phone);
|
||||
update();
|
||||
Get.back(); // Close the contacts dialog after selection.
|
||||
}
|
||||
|
||||
void fetchDriverStats() async {
|
||||
try {
|
||||
var response = await CRUD().get(link: AppLink.getInviteDriver, payload: {
|
||||
"driverId": box.read(BoxName.driverID),
|
||||
});
|
||||
if (response != 'failure') {
|
||||
var data = jsonDecode(response);
|
||||
driverInvitationData = data['message'];
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle error gracefully
|
||||
}
|
||||
}
|
||||
|
||||
void fetchDriverStatsPassengers() async {
|
||||
try {
|
||||
var response = await CRUD()
|
||||
.get(link: AppLink.getDriverInvitationToPassengers, payload: {
|
||||
"driverId": box.read(BoxName.driverID),
|
||||
});
|
||||
if (response != 'failure') {
|
||||
var data = jsonDecode(response);
|
||||
driverInvitationDataToPassengers = data['message'];
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle error gracefully
|
||||
}
|
||||
}
|
||||
|
||||
void onSelectDriverInvitation(int index) async {
|
||||
MyDialog().getDialog(
|
||||
int.parse((driverInvitationData[index]['countOfInvitDriver'])) < 100
|
||||
? '${'When'.tr} ${(driverInvitationData[index]['invitorName'])} ${"complete, you can claim your gift".tr} '
|
||||
: 'You deserve the gift'.tr,
|
||||
'${(driverInvitationData[index]['invitorName'])} ${(driverInvitationData[index]['countOfInvitDriver'])} / 100 ${'Trip'.tr}',
|
||||
() async {
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
if (int.parse((driverInvitationData[index]['countOfInvitDriver'])) <
|
||||
100) {
|
||||
Get.back();
|
||||
} else if (isAvailable) {
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment',
|
||||
// options: const AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
if ((driverInvitationData[index]['isGiftToken']).toString() ==
|
||||
'0') {
|
||||
Get.back();
|
||||
await CRUD().post(
|
||||
link: AppLink.updateInviteDriver,
|
||||
payload: {'id': (driverInvitationData[index]['id'])});
|
||||
await Get.find<CaptainWalletController>().addDriverPayment(
|
||||
'paymentMethod',
|
||||
('500'),
|
||||
'',
|
||||
);
|
||||
await Get.find<CaptainWalletController>()
|
||||
.addDriverWalletToInvitor(
|
||||
'paymentMethod',
|
||||
(driverInvitationData[index]['driverInviterId']),
|
||||
('500'),
|
||||
);
|
||||
NotificationCaptainController().addNotificationCaptain(
|
||||
driverInvitationData[index]['driverInviterId'].toString(),
|
||||
"You have got a gift for invitation".tr,
|
||||
'${"You have 500".tr} ${'SYP'.tr}',
|
||||
false);
|
||||
NotificationController().showNotification(
|
||||
"You have got a gift for invitation".tr,
|
||||
'${"You have 500".tr} ${'SYP'.tr}',
|
||||
'tone1',
|
||||
'');
|
||||
} else {
|
||||
Get.back();
|
||||
MyDialog().getDialog("You have got a gift".tr,
|
||||
"Share the app with another new driver".tr, () => Get.back());
|
||||
}
|
||||
} else {
|
||||
MyDialog()
|
||||
.getDialog('Authentication failed'.tr, '', () => Get.back());
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr,
|
||||
() => Get.back());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void onSelectPassengerInvitation(int index) async {
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
MyDialog().getDialog(
|
||||
int.parse(driverInvitationDataToPassengers[index]['countOfInvitDriver']
|
||||
.toString()) <
|
||||
3
|
||||
? '${'When'.tr} ${(driverInvitationDataToPassengers[index]['passengerName'].toString())} ${"complete, you can claim your gift".tr} '
|
||||
: 'You deserve the gift'.tr,
|
||||
'${(driverInvitationDataToPassengers[index]['passengerName'].toString())} ${driverInvitationDataToPassengers[index]['countOfInvitDriver']} / 3 ${'Trip'.tr}',
|
||||
() async {
|
||||
if (int.parse(driverInvitationDataToPassengers[index]
|
||||
['countOfInvitDriver']
|
||||
.toString()) <
|
||||
3) {
|
||||
Get.back();
|
||||
} else if (isAvailable) {
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment',
|
||||
// options: const AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
if (driverInvitationDataToPassengers[index]['isGiftToken']
|
||||
.toString() ==
|
||||
'0') {
|
||||
Get.back();
|
||||
await Get.find<CaptainWalletController>()
|
||||
.addDriverWallet('paymentMethod', '200', '200');
|
||||
await Get.find<CaptainWalletController>()
|
||||
.addDriverWalletToInvitor('paymentMethod',
|
||||
driverInvitationData[index]['driverInviterId'], '200');
|
||||
await CRUD().post(
|
||||
link: AppLink.updatePassengerGift,
|
||||
payload: {'id': driverInvitationDataToPassengers[index]['id']},
|
||||
);
|
||||
NotificationCaptainController().addNotificationCaptain(
|
||||
driverInvitationDataToPassengers[index]['passengerInviterId']
|
||||
.toString(),
|
||||
"You have got a gift for invitation".tr,
|
||||
'${"You have 200".tr} ${'SYP'.tr}',
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
Get.back();
|
||||
MyDialog().getDialog(
|
||||
"You have got a gift".tr,
|
||||
"Share the app with another new passenger".tr,
|
||||
() => Get.back(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
MyDialog()
|
||||
.getDialog('Authentication failed'.tr, '', () => Get.back());
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog(
|
||||
'Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr,
|
||||
() => Get.back());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Formats a phone number to the standard Syrian international format (+963...).
|
||||
String _formatSyrianPhoneNumber(String input) {
|
||||
String digitsOnly = input.replaceAll(RegExp(r'\D'), '');
|
||||
|
||||
if (digitsOnly.startsWith('09') && digitsOnly.length == 10) {
|
||||
return '963${digitsOnly.substring(1)}';
|
||||
}
|
||||
if (digitsOnly.length == 9 && digitsOnly.startsWith('9')) {
|
||||
return '963$digitsOnly';
|
||||
}
|
||||
return input; // Fallback for unrecognized formats
|
||||
}
|
||||
|
||||
String normalizeSyrianPhone(String input) {
|
||||
String phone = input.trim();
|
||||
|
||||
// احذف كل شيء غير أرقام
|
||||
phone = phone.replaceAll(RegExp(r'[^0-9]'), '');
|
||||
|
||||
// إذا يبدأ بـ 0 → احذفها
|
||||
if (phone.startsWith('0')) {
|
||||
phone = phone.substring(1);
|
||||
}
|
||||
|
||||
// إذا يبدأ بـ 963 مكررة → احذف التكرار
|
||||
while (phone.startsWith('963963')) {
|
||||
phone = phone.substring(3);
|
||||
}
|
||||
|
||||
// إذا يبدأ بـ 963 ولكن داخله كمان 963 → خليه مرة واحدة فقط
|
||||
if (phone.startsWith('963') && phone.length > 12) {
|
||||
phone = phone.substring(phone.length - 9); // آخر 9 أرقام
|
||||
}
|
||||
|
||||
// الآن إذا كان بلا 963 → أضفها
|
||||
if (!phone.startsWith('963')) {
|
||||
phone = '963' + phone;
|
||||
}
|
||||
|
||||
return phone;
|
||||
}
|
||||
|
||||
/// Sends an invitation to a potential new driver.
|
||||
void sendInvite() async {
|
||||
if (invitePhoneController.text.isEmpty) {
|
||||
mySnackeBarError('Please enter a phone number'.tr);
|
||||
return;
|
||||
}
|
||||
// Format Syrian phone number: remove leading 0 and add +963
|
||||
String formattedPhoneNumber =
|
||||
normalizeSyrianPhone(invitePhoneController.text);
|
||||
if (formattedPhoneNumber.length != 12) {
|
||||
mySnackeBarError('Please enter a correct phone'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
var response = await CRUD().post(link: AppLink.addInviteDriver, payload: {
|
||||
"driverId": box.read(BoxName.driverID),
|
||||
"inviterDriverPhone": formattedPhoneNumber,
|
||||
});
|
||||
|
||||
if (response != 'failure') {
|
||||
var d = (response);
|
||||
mySnackbarSuccess('Invite sent successfully'.tr);
|
||||
String message = '${'*Intaleq DRIVER CODE*'.tr}\n\n'
|
||||
'${"Use this code in registration".tr}\n'
|
||||
'${"To get a gift for both".tr}\n\n'
|
||||
'${"The period of this code is 24 hours".tr}\n\n'
|
||||
'${'before'.tr} *${d['message']['expirationTime'].toString()}*\n\n'
|
||||
'_*${d['message']['inviteCode'].toString()}*_\n\n'
|
||||
'${"Install our app:".tr}\n'
|
||||
'*Android:* https://play.google.com/store/apps/details?id=com.intaleq_driver \n\n\n'
|
||||
'*iOS:* https://apps.apple.com/st/app/intaleq-driver/id6482995159';
|
||||
|
||||
launchCommunication('whatsapp', formattedPhoneNumber, message);
|
||||
invitePhoneController.clear();
|
||||
} else {
|
||||
mySnackeBarError("Invite code already used".tr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends an invitation to a potential new passenger.
|
||||
void sendInviteToPassenger() async {
|
||||
if (invitePhoneController.text.isEmpty) {
|
||||
mySnackeBarError('Please enter a phone number'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
// Format Syrian phone number: remove leading 0 and add +963
|
||||
String formattedPhoneNumber = invitePhoneController.text.trim();
|
||||
if (formattedPhoneNumber.startsWith('0')) {
|
||||
formattedPhoneNumber = formattedPhoneNumber.substring(1);
|
||||
}
|
||||
formattedPhoneNumber = '+963$formattedPhoneNumber';
|
||||
|
||||
if (formattedPhoneNumber.length < 12) {
|
||||
// +963 + 9 digits = 12+
|
||||
mySnackeBarError('Please enter a correct phone'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
var response = await CRUD().post(
|
||||
link: AppLink.addInvitationPassenger,
|
||||
payload: {
|
||||
"driverId": box.read(BoxName.driverID),
|
||||
"inviterPassengerPhone": formattedPhoneNumber,
|
||||
},
|
||||
);
|
||||
|
||||
if (response != 'failure') {
|
||||
var d = response;
|
||||
mySnackbarSuccess('Invite sent successfully'.tr);
|
||||
|
||||
String message = '${'*Intaleq APP CODE*'.tr}\n\n'
|
||||
'${"Use this code in registration".tr}\n\n'
|
||||
'${"To get a gift for both".tr}\n\n'
|
||||
'${"The period of this code is 24 hours".tr}\n\n'
|
||||
'${'before'.tr} *${d['message']['expirationTime'].toString()}*\n\n'
|
||||
'_*${d['message']['inviteCode'].toString()}*_\n\n'
|
||||
'${"Install our app:".tr}\n'
|
||||
'*Android:* https://play.google.com/store/apps/details?id=com.Intaleq.intaleq\n\n\n'
|
||||
'*iOS:* https://apps.apple.com/st/app/intaleq-rider/id6748075179';
|
||||
|
||||
launchCommunication('whatsapp', formattedPhoneNumber, message);
|
||||
invitePhoneController.clear();
|
||||
} else {
|
||||
mySnackeBarError("Invite code already used".tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PassengerStats {
|
||||
final int totalInvites;
|
||||
final int activeUsers;
|
||||
final double totalEarnings;
|
||||
|
||||
PassengerStats({
|
||||
this.totalInvites = 0,
|
||||
this.activeUsers = 0,
|
||||
this.totalEarnings = 0.0,
|
||||
});
|
||||
}
|
||||
696
siro_driver/lib/controller/auth/captin/login_captin_controller.dart
Executable file
696
siro_driver/lib/controller/auth/captin/login_captin_controller.dart
Executable file
@@ -0,0 +1,696 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
import 'dart:math';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:siro_driver/views/auth/captin/cards/sms_signup.dart';
|
||||
import 'package:siro_driver/views/auth/syria/registration_view.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||
import 'package:location/location.dart';
|
||||
|
||||
import '../../../constant/api_key.dart';
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../print.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 '../../firebase/local_notification.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
import '../../functions/encrypt_decrypt.dart';
|
||||
import '../../functions/package_info.dart';
|
||||
import '../../functions/secure_storage.dart';
|
||||
import '../../functions/security_checks.dart';
|
||||
|
||||
class LoginDriverController extends GetxController {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
TextEditingController emailController = TextEditingController();
|
||||
TextEditingController phoneController = TextEditingController();
|
||||
TextEditingController passwordController = TextEditingController();
|
||||
TextEditingController passwordController2 = TextEditingController();
|
||||
bool isAgreeTerms = false;
|
||||
bool isGoogleDashOpen = false;
|
||||
bool isGoogleLogin = false;
|
||||
bool isloading = false;
|
||||
late int isTest = 1;
|
||||
final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
||||
final location = Location();
|
||||
void changeAgreeTerm() {
|
||||
isAgreeTerms = !isAgreeTerms;
|
||||
update();
|
||||
}
|
||||
|
||||
bool isPasswordHidden = true;
|
||||
|
||||
void togglePasswordVisibility() {
|
||||
isPasswordHidden = !isPasswordHidden;
|
||||
update([
|
||||
'passwordVisibility'
|
||||
]); // Use a unique ID to only update the password field
|
||||
}
|
||||
|
||||
void changeGoogleDashOpen() {
|
||||
isGoogleDashOpen = !isGoogleDashOpen;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
box.write(BoxName.countryCode, 'Syria');
|
||||
// box.write(BoxName.driverID, '34feffd3fa72d6bee56b');
|
||||
// await getAppTester();
|
||||
getJWT();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
getAppTester() async {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getTesterApp,
|
||||
payload: {'appPlatform': AppInformation.appName});
|
||||
// Log.print('res: ${res}');
|
||||
if (res != 'failure') {
|
||||
var d = jsonDecode(res);
|
||||
isTest = d['message'][0]['isTest'];
|
||||
// Log.print('isTest: ${isTest}');
|
||||
box.write(BoxName.isTest, isTest);
|
||||
|
||||
// Log.print('isTest: ${box.read(BoxName.isTest)}');
|
||||
update();
|
||||
} else {
|
||||
isTest = 0;
|
||||
box.write(BoxName.isTest, isTest);
|
||||
update();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
updateAppTester(String appPlatform) async {
|
||||
await CRUD().post(
|
||||
link: AppLink.updateTesterApp, payload: {'appPlatform': appPlatform});
|
||||
}
|
||||
|
||||
isPhoneVerified() async {
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.isPhoneVerified,
|
||||
payload: {'phone_number': box.read(BoxName.phoneDriver)});
|
||||
|
||||
if (res != 'failure') {
|
||||
// Get.offAll(() => SyrianCardAI());
|
||||
Get.offAll(() => RegistrationView());
|
||||
|
||||
// isloading = false;
|
||||
// update();
|
||||
} else {
|
||||
Get.offAll(() => PhoneNumberScreen());
|
||||
}
|
||||
}
|
||||
|
||||
void saveAgreementTerms() {
|
||||
box.write(BoxName.agreeTerms, 'agreed');
|
||||
update();
|
||||
}
|
||||
|
||||
void saveCountryCode(String countryCode) {
|
||||
box.write(BoxName.countryCode, countryCode);
|
||||
update();
|
||||
}
|
||||
|
||||
String shortHash(String password) {
|
||||
var bytes = utf8.encode(password);
|
||||
var digest = sha256.convert(bytes);
|
||||
return base64UrlEncode(digest.bytes);
|
||||
}
|
||||
|
||||
var dev = '';
|
||||
getJwtWallet() async {
|
||||
if (box.read(BoxName.security_check).toString() != 'passed') {
|
||||
Log.print('Security check failed');
|
||||
return;
|
||||
}
|
||||
Log.print('Security check passed');
|
||||
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||
dev = Platform.isAndroid ? 'android' : 'ios';
|
||||
var payload = {
|
||||
'id': box.read(BoxName.driverID),
|
||||
'password': AK.passnpassenger,
|
||||
'aud': '${AK.allowedWallet}$dev',
|
||||
'fingerPrint': fingerPrint
|
||||
};
|
||||
var response1 = await http.post(
|
||||
Uri.parse(AppLink.loginJwtWalletDriver),
|
||||
body: payload,
|
||||
);
|
||||
Log.print('response.request: ${response1.request}');
|
||||
Log.print('response.body: ${response1.body}');
|
||||
var decoded = jsonDecode(response1.body);
|
||||
var jwt = decoded['message'] is Map && decoded['message']['jwt'] != null ? decoded['message']['jwt'] : decoded['jwt'];
|
||||
var hmac = decoded['message'] is Map && decoded['message']['hmac'] != null ? decoded['message']['hmac'] : decoded['hmac'];
|
||||
Log.print('payment["jwt"]: $jwt');
|
||||
|
||||
await box.write(BoxName.hmac, hmac);
|
||||
return jwt.toString();
|
||||
}
|
||||
|
||||
getJWT() async {
|
||||
await EncryptionHelper.initialize();
|
||||
dev = Platform.isAndroid ? 'android' : 'ios';
|
||||
Log.print(
|
||||
'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
|
||||
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
|
||||
var payload = {
|
||||
'id': box.read(BoxName.driverID) ?? AK.newId,
|
||||
'password': AK.passnpassenger,
|
||||
'aud': '${AK.allowed}$dev',
|
||||
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
|
||||
await DeviceHelper.getDeviceFingerprint(),
|
||||
};
|
||||
// Log.print('payload: ${payload}');
|
||||
|
||||
var response0 = await http.post(
|
||||
Uri.parse(AppLink.loginFirstTimeDriver),
|
||||
body: payload,
|
||||
);
|
||||
Log.print('response0: ${response0.body}');
|
||||
Log.print('request: ${response0.request}');
|
||||
if (response0.statusCode == 200) {
|
||||
final decodedResponse1 = jsonDecode(response0.body);
|
||||
Log.print('decodedResponse1: ${decodedResponse1}');
|
||||
|
||||
String? jwt;
|
||||
if (decodedResponse1['message'] is Map && decodedResponse1['message']['jwt'] != null) {
|
||||
jwt = decodedResponse1['message']['jwt'];
|
||||
} else {
|
||||
jwt = decodedResponse1['jwt'];
|
||||
}
|
||||
|
||||
if (jwt != null) {
|
||||
box.write(BoxName.jwt, c(jwt));
|
||||
}
|
||||
|
||||
// ✅ بعد التأكد أن كل المفاتيح موجودة
|
||||
await EncryptionHelper.initialize();
|
||||
|
||||
// await AppInitializer().getKey();
|
||||
} else {}
|
||||
} else {
|
||||
await EncryptionHelper.initialize();
|
||||
|
||||
var payload = {
|
||||
'id': box.read(BoxName.driverID),
|
||||
'password': box.read(BoxName.emailDriver),
|
||||
'aud': '${AK.allowed}$dev',
|
||||
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
|
||||
await DeviceHelper.getDeviceFingerprint(),
|
||||
};
|
||||
// print(payload);
|
||||
var response1 = await http.post(
|
||||
Uri.parse(AppLink.loginJwtDriver),
|
||||
body: payload,
|
||||
);
|
||||
Log.print('response1.request: ${response1.request}');
|
||||
Log.print('response1.body: ${response1.body}');
|
||||
|
||||
if (response1.statusCode == 200) {
|
||||
final decodedResponse1 = jsonDecode(response1.body);
|
||||
// Log.print('decodedResponse1: ${decodedResponse1}');
|
||||
|
||||
String? jwt;
|
||||
if (decodedResponse1['message'] is Map && decodedResponse1['message']['jwt'] != null) {
|
||||
jwt = decodedResponse1['message']['jwt'];
|
||||
} else {
|
||||
jwt = decodedResponse1['jwt'];
|
||||
}
|
||||
|
||||
if (jwt != null) {
|
||||
await box.write(BoxName.jwt, c(jwt));
|
||||
}
|
||||
|
||||
// await AppInitializer().getKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getLocationPermission() async {
|
||||
var status = await Permission.locationAlways.status;
|
||||
if (!status.isGranted) {
|
||||
await Permission.locationAlways.request();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
String generateUniqueIdFromEmail(String email) {
|
||||
// Step 1: Extract the local part of the email
|
||||
String localPart = email.split('@')[0];
|
||||
|
||||
// Step 2: Replace invalid characters (if any)
|
||||
String cleanLocalPart = localPart.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '');
|
||||
|
||||
// Step 3: Ensure it does not exceed 24 characters
|
||||
if (cleanLocalPart.length > 24) {
|
||||
cleanLocalPart = cleanLocalPart.substring(0, 24);
|
||||
}
|
||||
|
||||
// Step 4: Generate a random suffix if needed
|
||||
String suffix = generateRandomSuffix(24 - cleanLocalPart.length);
|
||||
|
||||
return cleanLocalPart + suffix;
|
||||
}
|
||||
|
||||
String generateRandomSuffix(int length) {
|
||||
const String chars =
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
Random random = Random();
|
||||
return List.generate(length, (index) => chars[random.nextInt(chars.length)])
|
||||
.join('');
|
||||
}
|
||||
|
||||
bool isInviteDriverFound = false;
|
||||
|
||||
Future updateInvitationCodeFromRegister() async {
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.updateDriverInvitationDirectly,
|
||||
payload: {
|
||||
"inviterDriverPhone": box.read(BoxName.phoneDriver).toString(),
|
||||
},
|
||||
);
|
||||
Log.print('invite: ${res}');
|
||||
|
||||
// حماية من النوع — res قد يكون String ('failure'/'token_expired') بدل Map
|
||||
if (res is! Map) return;
|
||||
|
||||
if (res['status'] != 'failure') {
|
||||
isInviteDriverFound = true;
|
||||
update();
|
||||
box.write(BoxName.isInstall, '1');
|
||||
NotificationController().showNotification(
|
||||
"Code approved".tr, "Code approved".tr, 'tone2', '');
|
||||
|
||||
try {
|
||||
NotificationService.sendNotification(
|
||||
target: (res)['message'][0]['token'].toString(),
|
||||
title: 'You have received a gift token!'.tr,
|
||||
body: 'for '.tr + box.read(BoxName.phoneDriver).toString(),
|
||||
isTopic: false,
|
||||
tone: 'tone2',
|
||||
driverList: [], category: 'You have received a gift token!',
|
||||
);
|
||||
} catch (e) {
|
||||
Log.print('invite notification error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loginWithGoogleCredential(String driverID, email) async {
|
||||
isloading = true;
|
||||
update();
|
||||
// await SecurityHelper.performSecurityChecks();
|
||||
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
|
||||
// await getJWT();
|
||||
var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: {
|
||||
// 'email': email ?? 'yet',
|
||||
'id': driverID,
|
||||
});
|
||||
Log.print('loginWithGoogleCredential: ${res}');
|
||||
if (res == 'failure') {
|
||||
await isPhoneVerified();
|
||||
isloading = false; // <--- أضفت هذا أيضاً
|
||||
update();
|
||||
return false;
|
||||
// Get.snackbar('Failure', '', backgroundColor: Colors.red);
|
||||
} else {
|
||||
var jsonDecoeded = jsonDecode(res);
|
||||
var d = jsonDecoeded['data'][0];
|
||||
if (jsonDecoeded.isNotEmpty) {
|
||||
if (jsonDecoeded['status'] == 'success' &&
|
||||
d['is_verified'].toString() == '1') {
|
||||
box.write(BoxName.emailDriver, d['email']);
|
||||
box.write(BoxName.firstTimeLoadKey, 'false');
|
||||
box.write(BoxName.driverID, (d['id']));
|
||||
box.write(BoxName.isTest, '1');
|
||||
box.write(BoxName.gender, (d['gender']));
|
||||
box.write(BoxName.phoneVerified, d['is_verified'].toString());
|
||||
box.write(BoxName.phoneDriver, (d['phone']));
|
||||
box.write(BoxName.is_claimed, d['is_claimed']);
|
||||
box.write(BoxName.isInstall, d['isInstall']);
|
||||
// box.write(
|
||||
// BoxName.isGiftToken, d['isGiftToken']);
|
||||
box.write(BoxName.nameArabic, (d['name_arabic']));
|
||||
box.write(BoxName.carYear, d['year']);
|
||||
box.write(BoxName.bankCodeDriver, (d['bankCode']));
|
||||
box.write(BoxName.accountBankNumberDriver, (d['accountBank']));
|
||||
box.write(
|
||||
BoxName.nameDriver,
|
||||
'${(d['first_name'])}'
|
||||
' ${(d['last_name'])}');
|
||||
if (((d['model']).toString().contains('دراجه') ||
|
||||
d['make'].toString().contains('دراجه '))) {
|
||||
if ((d['gender']).toString() == 'Male') {
|
||||
box.write(BoxName.carTypeOfDriver, 'Scooter');
|
||||
} else {
|
||||
box.write(BoxName.carTypeOfDriver, 'Pink Bike');
|
||||
}
|
||||
} else if (int.parse(d['year'].toString()) > 2016) {
|
||||
if (d['gender'].toString() != 'Male') {
|
||||
box.write(BoxName.carTypeOfDriver, 'Lady');
|
||||
} else {
|
||||
box.write(BoxName.carTypeOfDriver, 'Comfort');
|
||||
}
|
||||
} else if (int.parse(d['year'].toString()) > 2002 &&
|
||||
int.parse(d['year'].toString()) < 2016) {
|
||||
box.write(BoxName.carTypeOfDriver, 'Speed');
|
||||
} else if (int.parse(d['year'].toString()) < 2002) {
|
||||
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
|
||||
}
|
||||
|
||||
// ✅ الحصول على توكن access بدل registration قبل أي طلبات بعد تسجيل الدخول
|
||||
Log.print('🔑 Getting access token after login...');
|
||||
await getJWT();
|
||||
Log.print('🔑 Access token obtained.');
|
||||
|
||||
// add invitations
|
||||
if (box.read(BoxName.isInstall) == null ||
|
||||
box.read(BoxName.isInstall).toString() == '0') {
|
||||
updateInvitationCodeFromRegister();
|
||||
}
|
||||
|
||||
// updateAppTester(AppInformation.appName);
|
||||
if (d['status'].toString() != 'yet') {
|
||||
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 (box.read(BoxName.emailDriver).toString() !=
|
||||
// '963992952235@intaleqapp.com') {
|
||||
if (token != 'failure') {
|
||||
var serverData = jsonDecode(token);
|
||||
if ((serverData['data'][0]['token'].toString()) !=
|
||||
box.read(BoxName.tokenDriver).toString() ||
|
||||
serverData['data'][0]['fingerPrint'].toString() !=
|
||||
fingerPrint.toString()) {
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'Device Change Detected'.tr,
|
||||
middleText: 'Please verify your identity'.tr,
|
||||
textConfirm: 'Verify'.tr,
|
||||
confirmTextColor: Colors.white,
|
||||
onConfirm: () {
|
||||
// نغلق الـ Dialog أولاً بشكل صريح
|
||||
if (Get.isDialogOpen ?? false) {
|
||||
Get.back();
|
||||
}
|
||||
|
||||
// ثم ننتقل لصفحة OTP
|
||||
Get.offAll(
|
||||
() => OtpVerificationPage(
|
||||
phone: d['phone'].toString(),
|
||||
deviceToken: fingerPrint.toString(),
|
||||
token: token.toString(),
|
||||
ptoken:
|
||||
jsonDecode(token)['data'][0]['token'].toString(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
isloading = false;
|
||||
update();
|
||||
return true; // نخرج من الدالة هنا لنسمح لـ OTP بالتعامل مع الأمر
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
Get.offAll(() => HomeCaptain()); // افترض أن هذا الكلاس موجود
|
||||
isloading = false; // <--- أضفت هذا
|
||||
update(); // <--- أضفت هذا
|
||||
return true;
|
||||
} else {
|
||||
Get.offAll(
|
||||
() => DriverVerificationScreen()); // افترض أن هذا الكلاس موجود
|
||||
isloading = false; // <--- أضفت هذا
|
||||
update(); // <--- أضفت هذا
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get.off(() => HomeCaptain());
|
||||
} else {
|
||||
Get.offAll(() => PhoneNumberScreen());
|
||||
isloading = false;
|
||||
update();
|
||||
return false; // <--- ✅ وهذا السطر موجود للحالات الأخرى
|
||||
}
|
||||
} else {
|
||||
mySnackbarSuccess('');
|
||||
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logintest(String driverID, email) async {
|
||||
isloading = true;
|
||||
update();
|
||||
// await SecurityHelper.performSecurityChecks();
|
||||
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
|
||||
|
||||
var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: {
|
||||
'email': email ?? 'yet',
|
||||
'id': driverID,
|
||||
});
|
||||
|
||||
// print('res is $res');
|
||||
// if (res == 'failure') {
|
||||
// await isPhoneVerified();
|
||||
// // Get.snackbar('Failure', '', backgroundColor: Colors.red);
|
||||
// } else
|
||||
// {
|
||||
var jsonDecoeded = jsonDecode(res);
|
||||
var d = jsonDecoeded['data'][0];
|
||||
if (jsonDecoeded.isNotEmpty) {
|
||||
if (jsonDecoeded['status'] == 'success')
|
||||
// &&
|
||||
// d['is_verified'].toString() == '1')
|
||||
{
|
||||
box.write(BoxName.emailDriver, d['email']);
|
||||
box.write(BoxName.firstTimeLoadKey, 'false');
|
||||
box.write(BoxName.driverID, (d['id']));
|
||||
box.write(BoxName.isTest, '1');
|
||||
box.write(BoxName.gender, (d['gender']));
|
||||
box.write(BoxName.phoneVerified, d['is_verified'].toString());
|
||||
box.write(BoxName.phoneDriver, (d['phone']));
|
||||
box.write(BoxName.is_claimed, d['is_claimed']);
|
||||
box.write(BoxName.isInstall, d['isInstall']);
|
||||
// box.write(
|
||||
// BoxName.isGiftToken, d['isGiftToken']);
|
||||
box.write(BoxName.nameArabic, (d['name_arabic']));
|
||||
box.write(BoxName.carYear, d['year']);
|
||||
box.write(BoxName.bankCodeDriver, (d['bankCode']));
|
||||
box.write(BoxName.accountBankNumberDriver, (d['accountBank']));
|
||||
box.write(
|
||||
BoxName.nameDriver,
|
||||
'${(d['first_name'])}'
|
||||
' ${(d['last_name'])}');
|
||||
if (((d['model']).toString().contains('دراجه') ||
|
||||
d['make'].toString().contains('دراجه '))) {
|
||||
if ((d['gender']).toString() == 'Male') {
|
||||
box.write(BoxName.carTypeOfDriver, 'Scooter');
|
||||
} else {
|
||||
box.write(BoxName.carTypeOfDriver, 'Pink Bike');
|
||||
}
|
||||
} else if (int.parse(d['year'].toString()) > 2016) {
|
||||
if (d['gender'].toString() != 'Male') {
|
||||
box.write(BoxName.carTypeOfDriver, 'Lady');
|
||||
} else {
|
||||
box.write(BoxName.carTypeOfDriver, 'Comfort');
|
||||
}
|
||||
} else if (int.parse(d['year'].toString()) > 2002 &&
|
||||
int.parse(d['year'].toString()) < 2016) {
|
||||
box.write(BoxName.carTypeOfDriver, 'Speed');
|
||||
} else if (int.parse(d['year'].toString()) < 2002) {
|
||||
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
|
||||
}
|
||||
// updateAppTester(AppInformation.appName);
|
||||
|
||||
// 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());
|
||||
|
||||
Get.off(() => HomeCaptain());
|
||||
// } else {
|
||||
// Get.offAll(() => PhoneNumberScreen());
|
||||
|
||||
// isloading = false;
|
||||
// update();
|
||||
// }
|
||||
// }
|
||||
// else {
|
||||
// mySnackbarSuccess('');
|
||||
|
||||
// isloading = false;
|
||||
// update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loginUsingCredentialsWithoutGoogle(String password, email) async {
|
||||
isloading = true;
|
||||
isGoogleLogin = true;
|
||||
update();
|
||||
var res = await CRUD()
|
||||
.get(link: AppLink.loginUsingCredentialsWithoutGoogle, payload: {
|
||||
'email': (email),
|
||||
'password': password,
|
||||
});
|
||||
box.write(BoxName.emailDriver, (email).toString());
|
||||
// print(res);
|
||||
if (res == 'failure') {
|
||||
//Failure
|
||||
if (box.read(BoxName.phoneVerified).toString() == '1') {
|
||||
// Get.offAll(() => SyrianCardAI());
|
||||
Get.offAll(() => RegistrationView());
|
||||
} else {
|
||||
Get.offAll(() => SmsSignupEgypt());
|
||||
}
|
||||
|
||||
isloading = false;
|
||||
update();
|
||||
} else {
|
||||
var jsonDecoeded = jsonDecode(res);
|
||||
var d = jsonDecoeded['data'][0];
|
||||
if (jsonDecoeded.isNotEmpty) {
|
||||
if (jsonDecoeded['status'] == 'success' &&
|
||||
d['is_verified'].toString() == '1') {
|
||||
box.write(BoxName.emailDriver, (d['email']));
|
||||
box.write(BoxName.driverID, (d['id']));
|
||||
box.write(BoxName.isTest, '1');
|
||||
box.write(BoxName.gender, (d['gender']));
|
||||
box.write(BoxName.phoneVerified, d['is_verified'].toString());
|
||||
box.write(BoxName.phoneDriver, (d['phone']));
|
||||
box.write(BoxName.nameArabic, (d['name_arabic']));
|
||||
box.write(BoxName.bankCodeDriver, (d['bankCode']));
|
||||
box.write(BoxName.accountBankNumberDriver, d['accountBank']);
|
||||
box.write(
|
||||
BoxName.nameDriver,
|
||||
'${(d['first_name'])}'
|
||||
' ${(d['last_name'])}');
|
||||
if ((d['model'].toString().contains('دراجه') ||
|
||||
d['make'].toString().contains('دراجه '))) {
|
||||
if ((d['gender']).toString() == 'Male') {
|
||||
box.write(BoxName.carTypeOfDriver, 'Scooter');
|
||||
} else {
|
||||
box.write(BoxName.carTypeOfDriver, 'Pink Bike');
|
||||
}
|
||||
} else if (int.parse(d['year'].toString()) > 2017) {
|
||||
if ((d['gender']).toString() != 'Male') {
|
||||
box.write(BoxName.carTypeOfDriver, 'Lady');
|
||||
} else {
|
||||
box.write(BoxName.carTypeOfDriver, 'Comfort');
|
||||
}
|
||||
} else if (int.parse(d['year'].toString()) > 2002 &&
|
||||
int.parse(d['year'].toString()) < 2017) {
|
||||
box.write(BoxName.carTypeOfDriver, 'Speed');
|
||||
} else if (int.parse(d['year'].toString()) < 2002) {
|
||||
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
|
||||
}
|
||||
updateAppTester(AppInformation.appName);
|
||||
|
||||
var fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||
await storage.write(key: BoxName.fingerPrint, value: fingerPrint);
|
||||
|
||||
var token = await CRUD().get(
|
||||
link: AppLink.getDriverToken,
|
||||
payload: {'captain_id': box.read(BoxName.driverID).toString()});
|
||||
|
||||
if (token != 'failure') {
|
||||
if ((jsonDecode(token)['data'][0]['token']) !=
|
||||
(box.read(BoxName.tokenDriver))) {
|
||||
// Get.put(FirebaseMessagesController()).sendNotificationToDriverMAP(
|
||||
// 'token change'.tr,
|
||||
// 'change device'.tr,
|
||||
// (jsonDecode(token)['data'][0]['token']).toString(),
|
||||
// [],
|
||||
// 'ding.wav');
|
||||
NotificationService.sendNotification(
|
||||
target: (jsonDecode(token)['data'][0]['token']).toString(),
|
||||
title: 'token change'.tr,
|
||||
body: 'token change'.tr,
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'cancel',
|
||||
driverList: [], category: 'token change',
|
||||
);
|
||||
Get.defaultDialog(
|
||||
title: 'you will use this device?'.tr,
|
||||
middleText: '',
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () async {
|
||||
await CRUD()
|
||||
.post(link: AppLink.addTokensDriver, payload: {
|
||||
'token': box.read(BoxName.tokenDriver),
|
||||
'captain_id': box.read(BoxName.driverID).toString(),
|
||||
'fingerPrint': (fingerPrint).toString()
|
||||
});
|
||||
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Get.off(() => HomeCaptain());
|
||||
// Get.off(() => LoginCaptin());
|
||||
} else {
|
||||
Get.offAll(() => SmsSignupEgypt());
|
||||
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError('');
|
||||
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void loginByBoxData() async {
|
||||
Get.to(() => HomeCaptain());
|
||||
await CRUD().post(link: AppLink.addTokensDriver, payload: {
|
||||
'token': box.read(BoxName.tokenDriver).toString(),
|
||||
'captain_id': box.read(BoxName.driverID).toString()
|
||||
});
|
||||
CRUD().post(
|
||||
link: "${AppLink.seferAlexandriaServer}/ride/firebase/addDriver.php",
|
||||
payload: {
|
||||
'token': box.read(BoxName.tokenDriver),
|
||||
'captain_id': box.read(BoxName.driverID).toString()
|
||||
});
|
||||
CRUD().post(
|
||||
link: "${AppLink.seferGizaServer}/ride/firebase/addDriver.php",
|
||||
payload: {
|
||||
'token': box.read(BoxName.tokenDriver),
|
||||
'captain_id': box.read(BoxName.driverID).toString()
|
||||
});
|
||||
}
|
||||
}
|
||||
93
siro_driver/lib/controller/auth/captin/ml_google_doc.dart
Executable file
93
siro_driver/lib/controller/auth/captin/ml_google_doc.dart
Executable file
@@ -0,0 +1,93 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
// import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/controller/functions/llama_ai.dart';
|
||||
|
||||
// class CarRegistrationRecognizerController extends GetxController {
|
||||
// @override
|
||||
// void onInit() {
|
||||
// // scanText();
|
||||
// super.onInit();
|
||||
// }
|
||||
|
||||
// // The ImagePicker instance
|
||||
// final ImagePicker _imagePicker = ImagePicker();
|
||||
|
||||
// // The GoogleMlKit TextRecognizer instance
|
||||
// // final TextRecognizer _textRecognizer = TextRecognizer();
|
||||
|
||||
// // The scanned text
|
||||
// String? scannedText;
|
||||
// String? jsonOutput;
|
||||
// final List<Map<String, dynamic>> lines = [];
|
||||
// Map extracted = {};
|
||||
// XFile? image;
|
||||
// CroppedFile? croppedFile;
|
||||
// // Picks an image from the camera or gallery and extracts the text
|
||||
// final List<Map<String, dynamic>> extractedTextWithCoordinates = [];
|
||||
|
||||
// Future<void> scanText() async {
|
||||
// // Pick an image from the camera or gallery
|
||||
// image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
// update();
|
||||
|
||||
// // If no image was picked, return
|
||||
// if (image == null) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Crop the image
|
||||
// croppedFile = await ImageCropper().cropImage(
|
||||
// sourcePath: image!.path,
|
||||
// //
|
||||
// uiSettings: [
|
||||
// AndroidUiSettings(
|
||||
// toolbarTitle: 'Cropper'.tr,
|
||||
// toolbarColor: AppColor.blueColor,
|
||||
// toolbarWidgetColor: AppColor.yellowColor,
|
||||
// initAspectRatio: CropAspectRatioPreset.original,
|
||||
// lockAspectRatio: false),
|
||||
// IOSUiSettings(
|
||||
// title: 'Cropper'.tr,
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
|
||||
// // If no cropped image was obtained, return
|
||||
// if (croppedFile == null) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Convert the cropped file to an InputImage object
|
||||
// final InputImage inputImage = InputImage.fromFile(File(croppedFile!.path));
|
||||
|
||||
// // Recognize the text in the image
|
||||
// final RecognizedText recognizedText =
|
||||
// await _textRecognizer.processImage(inputImage);
|
||||
// scannedText = recognizedText.text;
|
||||
|
||||
// // Extract the scanned text line by line
|
||||
// final List<Map<String, dynamic>> lines = [];
|
||||
// for (var i = 0; i < recognizedText.blocks.length; i++) {
|
||||
// lines.add({
|
||||
// i.toString(): recognizedText.blocks[i].text,
|
||||
// });
|
||||
// }
|
||||
|
||||
// String result = lines.map((map) => map.values.first.toString()).join(' ');
|
||||
// if (result.length > 2200) {
|
||||
// result = result.substring(0, 2200);
|
||||
// }
|
||||
// Map result2 = await LlamaAi().getCarRegistrationData(result,
|
||||
// 'vin,make,made,year,expiration_date,color,owner,registration_date'); //
|
||||
|
||||
// // Assign the result to the extracted variable
|
||||
// extracted = result2;
|
||||
|
||||
// update();
|
||||
// }
|
||||
// }
|
||||
126
siro_driver/lib/controller/auth/captin/opt_token_controller.dart
Normal file
126
siro_driver/lib/controller/auth/captin/opt_token_controller.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/print.dart';
|
||||
import 'package:siro_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../views/widgets/error_snakbar.dart';
|
||||
import '../../firebase/firbase_messge.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
import '../../functions/crud.dart';
|
||||
|
||||
class OtpVerificationController extends GetxController {
|
||||
final String phone;
|
||||
final String deviceToken;
|
||||
final String token;
|
||||
final otpCode = ''.obs;
|
||||
final isLoading = false.obs;
|
||||
final isVerifying = false.obs;
|
||||
var canResend = false.obs;
|
||||
var countdown = 120.obs;
|
||||
Timer? _timer;
|
||||
|
||||
OtpVerificationController({
|
||||
required this.phone,
|
||||
required this.deviceToken,
|
||||
required this.token,
|
||||
});
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
sendOtp(); // ترسل تلقائيًا عند فتح الصفحة
|
||||
startCountdown();
|
||||
}
|
||||
|
||||
void startCountdown() {
|
||||
canResend.value = false;
|
||||
countdown.value = 120;
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (countdown.value > 0) {
|
||||
countdown.value--;
|
||||
} else {
|
||||
canResend.value = true;
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> sendOtp() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
final response = await CRUD().post(
|
||||
link:
|
||||
'${AppLink.server}/auth/token_passenger/driver/send_otp_driver.php',
|
||||
payload: {
|
||||
'receiver': phone,
|
||||
// 'device_token': deviceToken,
|
||||
},
|
||||
);
|
||||
|
||||
if (response != 'failure') {
|
||||
// بإمكانك عرض رسالة نجاح هنا
|
||||
} else {
|
||||
// Get.snackbar('Error', 'Failed to send OTP');
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('Error', e.toString());
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> verifyOtp(String ptoken) async {
|
||||
isVerifying.value = true;
|
||||
var finger = box.read(BoxName.deviceFingerprint);
|
||||
try {
|
||||
final response = await CRUD().post(
|
||||
link:
|
||||
'${AppLink.server}/auth/token_passenger/driver/verify_otp_driver.php',
|
||||
payload: {
|
||||
'phone_number': phone,
|
||||
'otp': otpCode.value,
|
||||
'token': box.read(BoxName.tokenDriver).toString(),
|
||||
'fingerPrint': finger.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
if (response != 'failure' &&
|
||||
response != 'token_expired' &&
|
||||
response != 'no_internet') {
|
||||
Log.print('response (already decoded): ${response}');
|
||||
|
||||
// توجه إلى الصفحة التالية
|
||||
await CRUD().post(
|
||||
link: '${AppLink.paymentServer}/auth/token/update_driver_auth.php',
|
||||
payload: {
|
||||
'token': box.read(BoxName.tokenDriver).toString(),
|
||||
'fingerPrint': finger.toString(),
|
||||
'captain_id': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
|
||||
await NotificationService.sendNotification(
|
||||
target: ptoken.toString(),
|
||||
title: 'token change'.tr,
|
||||
body: 'token change'.tr,
|
||||
isTopic: false,
|
||||
tone: 'cancel',
|
||||
driverList: [],
|
||||
category: 'token change',
|
||||
);
|
||||
|
||||
Get.offAll(() => HomeCaptain());
|
||||
} else {
|
||||
mySnackeBarError('OTP is incorrect or expired'.tr);
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError(e.toString());
|
||||
} finally {
|
||||
isVerifying.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/print.dart';
|
||||
import 'package:siro_driver/views/home/on_boarding_page.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../views/auth/syria/registration_view.dart';
|
||||
|
||||
// --- Helper Class for Phone Authentication ---
|
||||
|
||||
class PhoneAuthHelper {
|
||||
// Define your server URLs
|
||||
static final String _baseUrl = '${AppLink.server}/auth/syria/driver/';
|
||||
static final String _sendOtpUrl = '${_baseUrl}sendWhatsAppDriver.php';
|
||||
static final String _verifyOtpUrl = '${_baseUrl}verifyOtp.php';
|
||||
static final String _registerUrl = '${_baseUrl}register_driver.php';
|
||||
static String formatSyrianPhone(String phone) {
|
||||
// Remove spaces, symbols, +, -, ()
|
||||
phone = phone.replaceAll(RegExp(r'[ \-\(\)\+]'), '').trim();
|
||||
|
||||
// Normalize 00963 → 963
|
||||
if (phone.startsWith('00963')) {
|
||||
phone = phone.replaceFirst('00963', '963');
|
||||
}
|
||||
|
||||
// Normalize 0963 → 963
|
||||
if (phone.startsWith('0963')) {
|
||||
phone = phone.replaceFirst('0963', '963');
|
||||
}
|
||||
if (phone.startsWith('096309')) {
|
||||
phone = phone.replaceFirst('096309', '963');
|
||||
}
|
||||
|
||||
// NEW: Fix 96309xxxx → 9639xxxx
|
||||
if (phone.startsWith('96309')) {
|
||||
phone = '9639' + phone.substring(5); // remove the "0" after 963
|
||||
}
|
||||
|
||||
// If starts with 9630 → correct to 9639
|
||||
if (phone.startsWith('9630')) {
|
||||
phone = '9639' + phone.substring(4);
|
||||
}
|
||||
|
||||
// If already in correct format: 9639xxxxxxxx
|
||||
if (phone.startsWith('9639') && phone.length == 12) {
|
||||
return phone;
|
||||
}
|
||||
|
||||
// If starts with 963 but missing the 9
|
||||
if (phone.startsWith('963') && phone.length > 3) {
|
||||
// Ensure it begins with 9639
|
||||
if (!phone.startsWith('9639')) {
|
||||
phone = '9639' + phone.substring(3);
|
||||
}
|
||||
return phone;
|
||||
}
|
||||
|
||||
// If starts with 09xxxxxxxx → 9639xxxxxxxx
|
||||
if (phone.startsWith('09')) {
|
||||
return '963' + phone.substring(1);
|
||||
}
|
||||
|
||||
// If 9xxxxxxxx (9 digits)
|
||||
if (phone.startsWith('9') && phone.length == 9) {
|
||||
return '963' + phone;
|
||||
}
|
||||
|
||||
// If starts with incorrect 0xxxxxxx → assume Syrian and fix
|
||||
if (phone.startsWith('0') && phone.length == 10) {
|
||||
return '963' + phone.substring(1);
|
||||
}
|
||||
|
||||
return phone;
|
||||
}
|
||||
|
||||
/// Sends an OTP to the provided phone number.
|
||||
static Future<bool> sendOtp(String phoneNumber) async {
|
||||
try {
|
||||
final fixedPhone = formatSyrianPhone(phoneNumber);
|
||||
Log.print('fixedPhone: $fixedPhone');
|
||||
|
||||
final response = await CRUD().post(
|
||||
link: _sendOtpUrl,
|
||||
payload: {'receiver': fixedPhone},
|
||||
);
|
||||
Log.print('fixedPhone: ${fixedPhone}');
|
||||
|
||||
if (response != 'failure') {
|
||||
final data = (response);
|
||||
Log.print('data: ${data}');
|
||||
// if (data['status'] == 'success') {
|
||||
mySnackbarSuccess('An OTP has been sent to your number.'.tr);
|
||||
return true;
|
||||
// } else {
|
||||
// mySnackeBarError(data['message'] ?? 'Failed to send OTP.');
|
||||
// return false;
|
||||
// }
|
||||
} else {
|
||||
mySnackeBarError('Server error. Please try again.'.tr);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
// mySnackeBarError('An error occurred: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies the OTP and logs the user in.
|
||||
static Future<void> verifyOtp(String phoneNumber, String otpCode) async {
|
||||
try {
|
||||
final fixedPhone = formatSyrianPhone(phoneNumber);
|
||||
Log.print('fixedPhone: $fixedPhone');
|
||||
final response = await CRUD().post(
|
||||
link: _verifyOtpUrl,
|
||||
payload: {
|
||||
'phone_number': fixedPhone,
|
||||
'otp': otpCode,
|
||||
},
|
||||
);
|
||||
|
||||
if (response != 'failure') {
|
||||
final data = response;
|
||||
|
||||
if (data['status'] == 'success') {
|
||||
final isRegistered = data['message']['isRegistered'] ?? false;
|
||||
box.write(BoxName.phoneVerified, '1');
|
||||
box.write(BoxName.phoneDriver, phoneNumber);
|
||||
box.write(BoxName.driverID, data['message']['driverID']);
|
||||
|
||||
if (isRegistered) {
|
||||
// ✅ السائق مسجل مسبقًا - سجل دخوله واذهب إلى الصفحة الرئيسية
|
||||
final driver = data['message']['driver'];
|
||||
// mySnackbarSuccess('Welcome back, ${driver['first_name']}!');
|
||||
|
||||
// حفظ بيانات السائق إذا أردت:
|
||||
box.write(BoxName.driverID, driver['id']);
|
||||
box.write(BoxName.emailDriver, driver['email']);
|
||||
|
||||
await Get.find<LoginDriverController>().loginWithGoogleCredential(
|
||||
driver['id'].toString(), driver['email'].toString());
|
||||
} else {
|
||||
// ✅ رقم الهاتف تم التحقق منه لكن السائق غير مسجل
|
||||
// mySnackbarSuccess('Phone verified. Please complete registration.');
|
||||
// Get.offAll(() => SyrianCardAI());
|
||||
Get.to(() => RegistrationView());
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError(data['message'] ?? 'Verification failed.');
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError('Server error. Please try again.');
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError('An error occurred: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> registerUser({
|
||||
required String phoneNumber,
|
||||
required String firstName,
|
||||
required String lastName,
|
||||
String? email,
|
||||
}) async {
|
||||
try {
|
||||
final response = await CRUD().post(
|
||||
link: _registerUrl,
|
||||
payload: {
|
||||
'phone_number': phoneNumber,
|
||||
'first_name': firstName,
|
||||
'last_name': lastName,
|
||||
'email': email ?? '', // Send empty string if null
|
||||
},
|
||||
);
|
||||
final data = (response);
|
||||
if (data != 'failure') {
|
||||
// Registration successful, log user in
|
||||
await _handleSuccessfulLogin(data['message']);
|
||||
} else {
|
||||
mySnackeBarError(
|
||||
"User with this phone number or email already exists.".tr);
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError('An error occurred: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _handleSuccessfulLogin(
|
||||
Map<String, dynamic> userData) async {
|
||||
mySnackbarSuccess('Welcome, ${userData['first_name']}!');
|
||||
|
||||
// Save user data to local storage (Hive box) using new keys
|
||||
box.write(BoxName.passengerID, userData['id']);
|
||||
box.write(BoxName.nameDriver, userData['first_name']);
|
||||
box.write(BoxName.lastNameDriver, userData['last_name']);
|
||||
box.write(BoxName.emailDriver, userData['email']);
|
||||
box.write(BoxName.phoneDriver, userData['phone']);
|
||||
|
||||
Get.offAll(() => OnBoardingPage()); // Navigate to home
|
||||
}
|
||||
}
|
||||
418
siro_driver/lib/controller/auth/captin/register_captin_controller.dart
Executable file
418
siro_driver/lib/controller/auth/captin/register_captin_controller.dart
Executable file
@@ -0,0 +1,418 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/controller/functions/ocr_controller.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/auth/captin/login_captin.dart';
|
||||
import 'package:siro_driver/views/auth/captin/verify_email_captain.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../views/auth/captin/ai_page.dart';
|
||||
import '../../../views/auth/syria/registration_view.dart';
|
||||
import '../../../views/home/Captin/home_captain/home_captin.dart';
|
||||
import '../../functions/sms_egypt_controller.dart';
|
||||
|
||||
class RegisterCaptainController extends GetxController {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final formKey3 = GlobalKey<FormState>();
|
||||
|
||||
TextEditingController emailController = TextEditingController();
|
||||
TextEditingController phoneController = TextEditingController();
|
||||
TextEditingController passwordController = TextEditingController();
|
||||
TextEditingController verifyCode = TextEditingController();
|
||||
|
||||
String birthDate = 'Birth Date'.tr;
|
||||
String gender = 'Male'.tr;
|
||||
bool isLoading = false;
|
||||
bool isSent = false;
|
||||
late String name;
|
||||
late String licenseClass;
|
||||
late String documentNo;
|
||||
late String address;
|
||||
late String height;
|
||||
late String postalCode;
|
||||
late String sex;
|
||||
late String stateCode;
|
||||
late String expireDate;
|
||||
late String dob;
|
||||
|
||||
getBirthDate() {
|
||||
Get.defaultDialog(
|
||||
title: 'Select Date'.tr,
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
child: CalendarDatePicker(
|
||||
initialDate: DateTime.now().subtract(const Duration(days: 18 * 365)),
|
||||
firstDate: DateTime.parse('1940-06-01'),
|
||||
lastDate: DateTime.now().subtract(const Duration(days: 18 * 365)),
|
||||
onDateChanged: (date) {
|
||||
// Get the selected date and convert it to a DateTime object
|
||||
DateTime dateTime = date;
|
||||
// Call the getOrders() function from the controller
|
||||
birthDate = dateTime.toString().split(' ')[0];
|
||||
update();
|
||||
Get.back();
|
||||
},
|
||||
|
||||
// onDateChanged: (DateTime value) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
// Get.put(SmsEgyptController());
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void changeGender(String value) {
|
||||
gender = value;
|
||||
update();
|
||||
}
|
||||
|
||||
bool isValidEgyptianPhoneNumber(String phoneNumber) {
|
||||
// Remove any non-digit characters (spaces, dashes, etc.)
|
||||
phoneNumber = phoneNumber.replaceAll(RegExp(r'\D+'), '');
|
||||
|
||||
// Check if the phone number has exactly 11 digits
|
||||
if (phoneNumber.length != 11) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the phone number starts with 010, 011, 012, or 015
|
||||
RegExp validPrefixes = RegExp(r'^01[0125]\d{8}$');
|
||||
|
||||
return validPrefixes.hasMatch(phoneNumber);
|
||||
}
|
||||
|
||||
sendOtpMessage() async {
|
||||
SmsEgyptController smsEgyptController = Get.put(SmsEgyptController());
|
||||
isLoading = true;
|
||||
update();
|
||||
isLoading = true;
|
||||
update();
|
||||
if (formKey3.currentState!.validate()) {
|
||||
if (box.read(BoxName.countryCode) == 'Egypt') {
|
||||
if (isValidEgyptianPhoneNumber(phoneController.text)) {
|
||||
var responseCheker = await CRUD()
|
||||
.post(link: AppLink.checkPhoneNumberISVerfiedDriver, payload: {
|
||||
'phone_number': ('+2${phoneController.text}'),
|
||||
});
|
||||
if (responseCheker != 'failure') {
|
||||
var d = jsonDecode(responseCheker);
|
||||
if (d['message'][0]['is_verified'].toString() == '1') {
|
||||
Get.snackbar('Phone number is verified before'.tr, '',
|
||||
backgroundColor: AppColor.greenColor);
|
||||
box.write(BoxName.phoneVerified, '1');
|
||||
box.write(BoxName.phone, ('+2${phoneController.text}'));
|
||||
await Get.put(LoginDriverController()).loginWithGoogleCredential(
|
||||
box.read(BoxName.driverID).toString(),
|
||||
(box.read(BoxName.emailDriver).toString()),
|
||||
);
|
||||
} else {
|
||||
await CRUD().post(link: AppLink.sendVerifyOtpMessage, payload: {
|
||||
'phone_number': ('+2${phoneController.text}'),
|
||||
"driverId": box.read(BoxName.driverID),
|
||||
"email": (box.read(BoxName.emailDriver)),
|
||||
});
|
||||
|
||||
isSent = true;
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
await CRUD().post(link: AppLink.sendVerifyOtpMessage, payload: {
|
||||
'phone_number': ('+2${phoneController.text}'),
|
||||
"driverId": box.read(BoxName.driverID),
|
||||
"email": box.read(BoxName.emailDriver),
|
||||
});
|
||||
|
||||
isSent = true;
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError(
|
||||
'Phone Number wrong'.tr,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
DateTime? lastOtpSentTime; // Store the last OTP sent time
|
||||
int otpResendInterval = 300; // 5 minutes in seconds
|
||||
|
||||
// Main function to handle OTP sending
|
||||
// sendOtpMessage() async {
|
||||
// if (_isOtpResendAllowed()) {
|
||||
// isLoading = true;
|
||||
// update();
|
||||
|
||||
// if (formKey3.currentState!.validate()) {
|
||||
// String countryCode = box.read(BoxName.countryCode);
|
||||
// String phoneNumber = phoneController.text;
|
||||
|
||||
// if (countryCode == 'Egypt' && isValidEgyptianPhoneNumber(phoneNumber)) {
|
||||
// await _checkAndSendOtp(phoneNumber);
|
||||
// } else {
|
||||
// _showErrorMessage('Phone Number is not Egypt phone '.tr);
|
||||
// }
|
||||
// }
|
||||
// isLoading = false;
|
||||
// update();
|
||||
// } else {
|
||||
// _showCooldownMessage();
|
||||
// }
|
||||
// }
|
||||
|
||||
// Check if the resend OTP request is allowed (5 minutes cooldown)
|
||||
// bool _isOtpResendAllowed() {
|
||||
// if (lastOtpSentTime == null) return true;
|
||||
|
||||
// final int elapsedTime =
|
||||
// DateTime.now().difference(lastOtpSentTime!).inSeconds;
|
||||
// return elapsedTime >= otpResendInterval;
|
||||
// }
|
||||
|
||||
// // Show message when user tries to resend OTP too soon
|
||||
// void _showCooldownMessage() {
|
||||
// int remainingTime = otpResendInterval -
|
||||
// DateTime.now().difference(lastOtpSentTime!).inSeconds;
|
||||
// Get.snackbar(
|
||||
// 'Please wait ${remainingTime ~/ 60}:${(remainingTime % 60).toString().padLeft(2, '0')} minutes before requesting again',
|
||||
// '',
|
||||
// backgroundColor: AppColor.redColor,
|
||||
// );
|
||||
// }
|
||||
|
||||
// // Check if the phone number has been verified, and send OTP if not verified
|
||||
// _checkAndSendOtp(String phoneNumber) async {
|
||||
// var responseChecker = await CRUD().post(
|
||||
// link: AppLink.checkPhoneNumberISVerfiedDriver,
|
||||
// payload: {
|
||||
// 'phone_number': '+2$phoneNumber',
|
||||
// },
|
||||
// );
|
||||
|
||||
// if (responseChecker != 'failure') {
|
||||
// var responseData = jsonDecode(responseChecker);
|
||||
// if (_isPhoneVerified(responseData)) {
|
||||
// _handleAlreadyVerified();
|
||||
// } else {
|
||||
// await _sendOtpAndSms(phoneNumber);
|
||||
// }
|
||||
// } else {
|
||||
// await _sendOtpAndSms(phoneNumber);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Check if the phone number is already verified
|
||||
bool _isPhoneVerified(dynamic responseData) {
|
||||
return responseData['message'][0]['is_verified'].toString() == '1';
|
||||
}
|
||||
|
||||
// Handle case where phone number is already verified
|
||||
_handleAlreadyVerified() {
|
||||
mySnackbarSuccess('Phone number is already verified'.tr);
|
||||
box.write(BoxName.phoneVerified, '1');
|
||||
box.write(BoxName.phone, ('+2${phoneController.text}'));
|
||||
Get.put(LoginDriverController()).loginWithGoogleCredential(
|
||||
box.read(BoxName.driverID).toString(),
|
||||
box.read(BoxName.emailDriver).toString(),
|
||||
);
|
||||
}
|
||||
|
||||
// Send OTP and SMS
|
||||
_sendOtpAndSms(String phoneNumber) async {
|
||||
SmsEgyptController smsEgyptController = Get.put(SmsEgyptController());
|
||||
int randomNumber = Random().nextInt(100000) + 1;
|
||||
|
||||
await CRUD().post(
|
||||
link: AppLink.sendVerifyOtpMessage,
|
||||
payload: {
|
||||
'phone_number': ('+2$phoneNumber'),
|
||||
'token_code': (randomNumber.toString()),
|
||||
'driverId': box.read(BoxName.driverID),
|
||||
'email': box.read(BoxName.emailDriver),
|
||||
},
|
||||
);
|
||||
|
||||
await smsEgyptController.sendSmsEgypt(phoneNumber);
|
||||
|
||||
lastOtpSentTime = DateTime.now(); // Update the last OTP sent time
|
||||
isSent = true;
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
verifySMSCode() async {
|
||||
// var loginDriverController = Get.put(LoginDriverController());
|
||||
if (formKey3.currentState!.validate()) {
|
||||
var res = await CRUD().post(link: AppLink.verifyOtpDriver, payload: {
|
||||
'phone_number': ('+2${phoneController.text}'),
|
||||
'token_code': (verifyCode.text.toString()),
|
||||
});
|
||||
if (res != 'failure') {
|
||||
// var dec = jsonDecode(res);
|
||||
box.write(BoxName.phoneDriver, ('+2${phoneController.text}'));
|
||||
box.write(BoxName.phoneVerified, '1');
|
||||
|
||||
// loginDriverController.isGoogleLogin == true
|
||||
// ? await loginDriverController.loginUsingCredentialsWithoutGoogle(
|
||||
// loginDriverController.passwordController.text.toString(),
|
||||
// box.read(BoxName.emailDriver).toString(),
|
||||
// )
|
||||
// : await loginDriverController.loginUsingCredentials(
|
||||
// box.read(BoxName.driverID).toString(),
|
||||
// box.read(BoxName.emailDriver).toString(),
|
||||
// );
|
||||
// Get.offAll(() => SyrianCardAI());
|
||||
Get.to(() => RegistrationView());
|
||||
// } else {
|
||||
// Get.snackbar('title', 'message');
|
||||
// }
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError('you must insert token code '.tr);
|
||||
}
|
||||
}
|
||||
|
||||
sendVerifications() async {
|
||||
var res = await CRUD().post(link: AppLink.verifyEmail, payload: {
|
||||
'email': emailController.text.isEmpty
|
||||
? (Get.find<LoginDriverController>().emailController.text.toString())
|
||||
: (emailController.text),
|
||||
'token': (verifyCode.text),
|
||||
});
|
||||
|
||||
if (res != 'failure') {
|
||||
if (Get.find<LoginDriverController>().emailController.text.toString() !=
|
||||
'') {
|
||||
Get.offAll(() => HomeCaptain());
|
||||
} else {
|
||||
// Get.to(() => CarLicensePage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void nextToAIDetection() async {
|
||||
//Todo dont forget this
|
||||
if (formKey.currentState!.validate()) {
|
||||
isLoading = true;
|
||||
update();
|
||||
Get.to(() => AiPage());
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> payloadLisence = {};
|
||||
|
||||
void getFromController() {
|
||||
name = Get.find<ScanDocumentsByApi>().name;
|
||||
licenseClass = Get.find<ScanDocumentsByApi>().licenseClass.toString();
|
||||
documentNo = Get.find<ScanDocumentsByApi>().documentNo.toString();
|
||||
address = Get.find<ScanDocumentsByApi>().address.toString();
|
||||
height = Get.find<ScanDocumentsByApi>().height.toString();
|
||||
postalCode = Get.find<ScanDocumentsByApi>().address.toString();
|
||||
sex = Get.find<ScanDocumentsByApi>().sex.toString();
|
||||
stateCode = Get.find<ScanDocumentsByApi>().postalCode.toString();
|
||||
expireDate = Get.find<ScanDocumentsByApi>().expireDate.toString();
|
||||
dob = Get.find<ScanDocumentsByApi>().dob.toString();
|
||||
update();
|
||||
}
|
||||
|
||||
Future addLisence() async {
|
||||
getFromController();
|
||||
var res = await CRUD().post(link: AppLink.addLicense, payload: {
|
||||
'name': name,
|
||||
'licenseClass': licenseClass,
|
||||
'documentNo': documentNo,
|
||||
'address': address,
|
||||
'height': height,
|
||||
'postalCode': postalCode,
|
||||
'sex': sex,
|
||||
'stateCode': stateCode,
|
||||
'expireDate': expireDate,
|
||||
'dateOfBirth': dob,
|
||||
});
|
||||
isLoading = false;
|
||||
update();
|
||||
if (jsonDecode(res)['status'] == 'success') {
|
||||
// Get.to(() => AiPage()); //todo rplace this
|
||||
}
|
||||
}
|
||||
|
||||
void addRegisrationCarForDriver(String vin, make, model, year, color, owner,
|
||||
expirationDate, registrationDate) async {
|
||||
getFromController();
|
||||
var res = await CRUD().post(link: AppLink.addRegisrationCar, payload: {
|
||||
'vin': vin,
|
||||
'make': make,
|
||||
'model': model,
|
||||
'year': year,
|
||||
'expirationDate': expirationDate,
|
||||
'color': color,
|
||||
'owner': owner,
|
||||
'registrationDate': registrationDate,
|
||||
});
|
||||
box.write(BoxName.vin, vin);
|
||||
box.write(BoxName.make, make);
|
||||
box.write(BoxName.model, model);
|
||||
box.write(BoxName.year, year);
|
||||
box.write(BoxName.expirationDate, expirationDate);
|
||||
box.write(BoxName.color, color);
|
||||
box.write(BoxName.owner, owner);
|
||||
box.write(BoxName.registrationDate, registrationDate);
|
||||
isLoading = false;
|
||||
update();
|
||||
if (jsonDecode(res)['status'] == 'success') {
|
||||
Get.offAll(() => LoginCaptin()); //todo replace this
|
||||
}
|
||||
}
|
||||
|
||||
Future register() async {
|
||||
getFromController();
|
||||
if (formKey.currentState!.validate()) {
|
||||
isLoading = true;
|
||||
update();
|
||||
var res = await CRUD().post(link: AppLink.signUpCaptin, payload: {
|
||||
'first_name': name.split(' ')[1],
|
||||
'last_name': name.split(' ')[0],
|
||||
'email': emailController.text,
|
||||
'phone': phoneController.text,
|
||||
'password': passwordController.text,
|
||||
'gender': sex,
|
||||
'site': address,
|
||||
'birthdate': dob,
|
||||
});
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
if (jsonDecode(res)['status'] == 'success') {
|
||||
box.write(BoxName.driverID, jsonDecode(res)['message']);
|
||||
box.write(BoxName.dobDriver, dob);
|
||||
box.write(BoxName.sexDriver, sex);
|
||||
box.write(BoxName.phoneDriver, phoneController.text);
|
||||
box.write(BoxName.lastNameDriver, name.split(' ')[0]);
|
||||
int randomNumber = Random().nextInt(100000) + 1;
|
||||
await CRUD().post(link: AppLink.sendVerifyEmail, payload: {
|
||||
'email': emailController.text,
|
||||
'token': randomNumber.toString(),
|
||||
});
|
||||
Get.to(() => VerifyEmailCaptainPage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
siro_driver/lib/controller/auth/facebook_login.dart
Executable file
30
siro_driver/lib/controller/auth/facebook_login.dart
Executable file
@@ -0,0 +1,30 @@
|
||||
// import 'package:firebase_auth/firebase_auth.dart';
|
||||
// import 'package:flutter_facebook_auth/flutter_facebook_auth.dart';
|
||||
|
||||
// class FacebookSignIn {
|
||||
// Future<UserCredential?> signInWithFacebook() async {
|
||||
// final LoginResult result = await FacebookAuth.instance.login();
|
||||
// if (result.status == LoginStatus.success) {
|
||||
// // Create a credential from the access token
|
||||
// final OAuthCredential credential =
|
||||
// FacebookAuthProvider.credential(result.accessToken!.tokenString);
|
||||
// // Once signed in, return the UserCredential
|
||||
// return await FirebaseAuth.instance.signInWithCredential(credential);
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// Future<void> signOut() async {
|
||||
// try {
|
||||
// await FacebookAuth.instance.logOut();
|
||||
// print('Facebook Sign Out Successful');
|
||||
// } catch (e) {
|
||||
// print('Error during Facebook Sign Out: $e');
|
||||
// }
|
||||
// }
|
||||
|
||||
// Future<bool> isSignedIn() async {
|
||||
// final accessToken = await FacebookAuth.instance.accessToken;
|
||||
// return accessToken != null;
|
||||
// }
|
||||
// }
|
||||
98
siro_driver/lib/controller/auth/google_sign.dart
Executable file
98
siro_driver/lib/controller/auth/google_sign.dart
Executable file
@@ -0,0 +1,98 @@
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/auth/captin/cards/sms_signup.dart';
|
||||
import 'package:siro_driver/views/home/on_boarding_page.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
|
||||
import '../../views/auth/captin/ai_page.dart';
|
||||
import '../functions/add_error.dart';
|
||||
import '../functions/encrypt_decrypt.dart';
|
||||
|
||||
class GoogleSignInHelper {
|
||||
static final GoogleSignIn _googleSignIn = GoogleSignIn.instance;
|
||||
|
||||
// متغير ثابت لحفظ حالة المستخدم محلياً كبديل لخاصية currentUser المحذوفة
|
||||
static GoogleSignInAccount? _cachedUser;
|
||||
|
||||
static Future<GoogleSignInAccount?> signIn() async {
|
||||
try {
|
||||
final GoogleSignInAccount? googleUser =
|
||||
await _googleSignIn.authenticate();
|
||||
|
||||
if (googleUser != null) {
|
||||
_cachedUser = googleUser; // حفظ الجلسة في الكاش المحلي
|
||||
await _handleSignUp(googleUser);
|
||||
|
||||
if (box.read(BoxName.countryCode) == 'Egypt') {
|
||||
Get.to(() => SmsSignupEgypt());
|
||||
} else if (box.read(BoxName.countryCode) == 'Jordan') {
|
||||
Get.to(() => AiPage());
|
||||
}
|
||||
}
|
||||
return googleUser;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<GoogleSignInAccount?> signInFromLogin() async {
|
||||
try {
|
||||
final GoogleSignInAccount? googleUser =
|
||||
await _googleSignIn.authenticate();
|
||||
|
||||
if (googleUser != null) {
|
||||
_cachedUser = googleUser; // حفظ الجلسة في الكاش المحلي
|
||||
await _handleSignUp(googleUser);
|
||||
|
||||
final driverID =
|
||||
(box.read(BoxName.driverID)?.toString()) ?? 'Unknown ID';
|
||||
final emailDriver =
|
||||
(box.read(BoxName.emailDriver)?.toString()) ?? 'Unknown Email';
|
||||
|
||||
print('Driver ID: $driverID');
|
||||
print('Driver Email: $emailDriver');
|
||||
|
||||
await Get.find<LoginDriverController>()
|
||||
.loginWithGoogleCredential(driverID, emailDriver);
|
||||
}
|
||||
|
||||
return googleUser;
|
||||
} catch (error, stackTrace) {
|
||||
mySnackeBarError('$error');
|
||||
CRUD.addError(error.toString(), stackTrace.toString(),
|
||||
'GoogleSignInAccount?> signInFromLogin()');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _handleSignUp(GoogleSignInAccount user) async {
|
||||
box.write(BoxName.driverID, (user.id) ?? 'Unknown ID');
|
||||
box.write(BoxName.emailDriver, (user.email) ?? 'Unknown Email');
|
||||
}
|
||||
|
||||
static Future<void> signOut() async {
|
||||
try {
|
||||
await _googleSignIn.signOut();
|
||||
} catch (error) {
|
||||
// التعامل مع الخطأ بصمت إذا كانت جلسة جوجل فارغة مسبقاً
|
||||
} finally {
|
||||
_cachedUser = null; // مسح الكاش المحلي بشكل إلزامي
|
||||
await _handleSignOut();
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _handleSignOut() async {
|
||||
box.erase();
|
||||
storage.deleteAll();
|
||||
Get.offAll(OnBoardingPage());
|
||||
}
|
||||
|
||||
// استخدام الكاش المحلي بدلاً من استدعاء المكتبة
|
||||
static GoogleSignInAccount? getCurrentUser() {
|
||||
return _cachedUser;
|
||||
}
|
||||
}
|
||||
117
siro_driver/lib/controller/auth/login_controller.dart
Executable file
117
siro_driver/lib/controller/auth/login_controller.dart
Executable file
@@ -0,0 +1,117 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/controller/functions/secure_storage.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/auth/verify_email_page.dart';
|
||||
|
||||
import '../functions/encrypt_decrypt.dart';
|
||||
|
||||
class LoginController extends GetxController {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final formKeyAdmin = GlobalKey<FormState>();
|
||||
TextEditingController emailController = TextEditingController();
|
||||
TextEditingController phoneController = TextEditingController();
|
||||
TextEditingController passwordController = TextEditingController();
|
||||
TextEditingController adminPasswordController = TextEditingController();
|
||||
TextEditingController adminNameController = TextEditingController();
|
||||
bool isAgreeTerms = false;
|
||||
bool isloading = false;
|
||||
final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
||||
|
||||
void changeAgreeTerm() {
|
||||
isAgreeTerms = !isAgreeTerms;
|
||||
update();
|
||||
}
|
||||
|
||||
void saveAgreementTerms() {
|
||||
box.write(BoxName.agreeTerms, 'agreed');
|
||||
update();
|
||||
}
|
||||
|
||||
void saveCountryCode(String countryCode) {
|
||||
box.write(BoxName.countryCode, countryCode);
|
||||
update();
|
||||
}
|
||||
|
||||
void login() async {
|
||||
isloading = true;
|
||||
update();
|
||||
var res = await CRUD().get(link: AppLink.login, payload: {
|
||||
'email': emailController.text,
|
||||
'phone': phoneController.text,
|
||||
'password': passwordController.text
|
||||
});
|
||||
isloading = false;
|
||||
update();
|
||||
if (res == 'failure') {
|
||||
//Failure
|
||||
mySnackeBarError('');
|
||||
} else {
|
||||
var jsonDecoeded = jsonDecode(res);
|
||||
if (jsonDecoeded.isNotEmpty) {
|
||||
if (jsonDecoeded['status'] == 'success') {
|
||||
if (jsonDecoeded['data'][0]['verified'] == 1) {
|
||||
box.write(BoxName.driverID, jsonDecoeded['data'][0]['id']);
|
||||
box.write(BoxName.emailDriver, (jsonDecoeded['data'][0]['email']));
|
||||
box.write(
|
||||
BoxName.nameDriver,
|
||||
jsonDecoeded['data'][0]['first_name'] +
|
||||
' ' +
|
||||
jsonDecoeded['data'][0]['last_name']);
|
||||
box.write(BoxName.phone, jsonDecoeded['data'][0]['phone']);
|
||||
SecureStorage().saveData(BoxName.password, passwordController.text);
|
||||
// Get.offAll(() => const MapPagePassenger());
|
||||
isloading = false;
|
||||
update();
|
||||
await CRUD().post(link: AppLink.addTokens, payload: {
|
||||
'token': box.read(BoxName.tokenFCM),
|
||||
'passengerID': box.read(BoxName.passengerID).toString()
|
||||
});
|
||||
} else {
|
||||
isloading = false;
|
||||
update();
|
||||
Get.defaultDialog(
|
||||
title: 'You must Verify email !.'.tr,
|
||||
middleText: '',
|
||||
backgroundColor: Colors.yellow[300],
|
||||
onConfirm: () async {
|
||||
int randomNumber = Random().nextInt(100000) + 1;
|
||||
await CRUD().post(link: AppLink.sendVerifyEmail, payload: {
|
||||
'email': emailController.text,
|
||||
'token': randomNumber.toString(),
|
||||
});
|
||||
Get.to(() => const VerifyEmailPage());
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if (jsonDecoeded['status'] == 'Failure') {
|
||||
mySnackeBarError(jsonDecoeded['data']);
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
goToMapPage() {
|
||||
if (box.read(BoxName.email) != null) {
|
||||
// Get.offAll(() => const MapPagePassenger());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
43
siro_driver/lib/controller/auth/onboarding_controller.dart
Executable file
43
siro_driver/lib/controller/auth/onboarding_controller.dart
Executable file
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
|
||||
import '../../models/model/onboarding_model.dart';
|
||||
import '../../views/auth/captin/login_captin.dart';
|
||||
|
||||
abstract class OnBoardingController extends GetxController {
|
||||
next();
|
||||
onPageChanged(int index);
|
||||
}
|
||||
|
||||
class OnBoardingControllerImp extends OnBoardingController {
|
||||
late PageController pageController;
|
||||
|
||||
int currentPage = 0;
|
||||
|
||||
@override
|
||||
next() {
|
||||
currentPage++;
|
||||
|
||||
if (currentPage > onBoardingList.length - 1) {
|
||||
box.write(BoxName.onBoarding, 'yes');
|
||||
Get.offAll(() => LoginCaptin());
|
||||
} else {
|
||||
pageController.animateToPage(currentPage,
|
||||
duration: const Duration(milliseconds: 900), curve: Curves.easeInOut);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
onPageChanged(int index) {
|
||||
currentPage = index;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
pageController = PageController();
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
95
siro_driver/lib/controller/auth/register_controller.dart
Executable file
95
siro_driver/lib/controller/auth/register_controller.dart
Executable file
@@ -0,0 +1,95 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
|
||||
import '../../views/auth/captin/login_captin.dart';
|
||||
import '../../views/auth/verify_email_page.dart';
|
||||
|
||||
class RegisterController extends GetxController {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
TextEditingController firstNameController = TextEditingController();
|
||||
TextEditingController lastNameController = TextEditingController();
|
||||
TextEditingController emailController = TextEditingController();
|
||||
TextEditingController phoneController = TextEditingController();
|
||||
TextEditingController passwordController = TextEditingController();
|
||||
TextEditingController siteController = TextEditingController();
|
||||
TextEditingController verfyCode = TextEditingController();
|
||||
|
||||
String birthDate = 'Birth Date'.tr;
|
||||
String gender = 'Male'.tr;
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
getBirthDate() {
|
||||
Get.defaultDialog(
|
||||
title: 'Select Date'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
child: CalendarDatePicker(
|
||||
initialDate:
|
||||
DateTime.now().subtract(const Duration(days: 14 * 365)),
|
||||
firstDate: DateTime.parse('1940-06-01'),
|
||||
lastDate: DateTime.now().subtract(const Duration(days: 14 * 365)),
|
||||
onDateChanged: (date) {
|
||||
// Get the selected date and convert it to a DateTime object
|
||||
DateTime dateTime = date;
|
||||
// Call the getOrders() function from the controller
|
||||
birthDate = dateTime.toString().split(' ')[0];
|
||||
update();
|
||||
},
|
||||
|
||||
// onDateChanged: (DateTime value) {},
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(title: 'Ok'.tr, onPressed: () => Get.back()));
|
||||
}
|
||||
|
||||
void changeGender(String value) {
|
||||
gender = value;
|
||||
update();
|
||||
}
|
||||
|
||||
sendVerifications() async {
|
||||
var res = await CRUD().post(link: AppLink.verifyEmail, payload: {
|
||||
'email': emailController.text,
|
||||
'token': verfyCode.text,
|
||||
});
|
||||
var dec = jsonDecode(res);
|
||||
if (dec['status'] == 'success') {
|
||||
Get.offAll(() => LoginCaptin());
|
||||
}
|
||||
}
|
||||
|
||||
void register() async {
|
||||
if (formKey.currentState!.validate()) {
|
||||
var res = await CRUD().post(link: AppLink.signUp, payload: {
|
||||
'first_name': firstNameController.text.toString(),
|
||||
'last_name': lastNameController.text.toString(),
|
||||
'email': emailController.text.toString(),
|
||||
'phone': phoneController.text.toString(),
|
||||
'password': passwordController.text.toString(),
|
||||
'gender': 'yet',
|
||||
'site': siteController.text,
|
||||
'birthdate': birthDate,
|
||||
});
|
||||
if (jsonDecode(res)['status'] == 'success') {
|
||||
int randomNumber = Random().nextInt(100000) + 1;
|
||||
await CRUD().post(link: AppLink.sendVerifyEmail, payload: {
|
||||
'email': emailController.text,
|
||||
'token': randomNumber.toString(),
|
||||
});
|
||||
Get.to(() => const VerifyEmailPage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,799 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
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:path/path.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/firebase/notification_service.dart';
|
||||
import '../../../constant/box_name.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
// --- Final Submission ---
|
||||
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../../views/widgets/error_snakbar.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/encrypt_decrypt.dart';
|
||||
import '../../functions/package_info.dart';
|
||||
import '../captin/login_captin_controller.dart';
|
||||
|
||||
// You can create a simple enum to manage image types
|
||||
enum ImageType {
|
||||
driverLicenseFront,
|
||||
driverLicenseBack,
|
||||
carLicenseFront,
|
||||
carLicenseBack,
|
||||
}
|
||||
|
||||
class RegistrationController extends GetxController {
|
||||
// Page Controller for managing steps
|
||||
late PageController pageController;
|
||||
var currentPage =
|
||||
0.obs; // Use .obs for reactive updates on the step indicator
|
||||
|
||||
// Loading state
|
||||
var isLoading = false.obs;
|
||||
var isloading = false;
|
||||
CroppedFile? croppedFile;
|
||||
final picker = ImagePicker();
|
||||
var image;
|
||||
File? myImage;
|
||||
String? colorHex; // سيُملى من الدروب داون
|
||||
// Form Keys for validation
|
||||
final driverInfoFormKey = GlobalKey<FormState>();
|
||||
final carInfoFormKey = GlobalKey<FormState>();
|
||||
|
||||
// STEP 1: Driver Information Controllers
|
||||
final firstNameController = TextEditingController();
|
||||
final lastNameController = TextEditingController();
|
||||
final nationalIdController = TextEditingController();
|
||||
final bithdateController = TextEditingController();
|
||||
final phoneController = TextEditingController(); // You can pre-fill this
|
||||
final driverLicenseExpiryController = TextEditingController();
|
||||
DateTime? driverLicenseExpiryDate;
|
||||
|
||||
// STEP 2: Car Information Controllers
|
||||
final carPlateController = TextEditingController();
|
||||
final carMakeController = TextEditingController();
|
||||
final carModelController = TextEditingController();
|
||||
final carYearController = TextEditingController();
|
||||
final carColorController = TextEditingController();
|
||||
final carVinController = TextEditingController(); // Chassis number
|
||||
final carRegistrationExpiryController = TextEditingController();
|
||||
DateTime? carRegistrationExpiryDate;
|
||||
// داخل RegistrationController
|
||||
|
||||
// المتغيرات لتخزين القيم المختارة (لإرسالها للـ API لاحقاً)
|
||||
int? selectedVehicleCategoryId; // سيخزن 1 أو 2 أو 3
|
||||
int? selectedFuelTypeId; // سيخزن 1 أو 2 أو 3 أو 4
|
||||
|
||||
// قائمة أنواع المركبات (مطابقة لقاعدة البيانات)
|
||||
final List<Map<String, dynamic>> vehicleCategoryOptions = [
|
||||
{'id': 1, 'name': 'Car'.tr}, // ترجمة: سيارة
|
||||
{'id': 2, 'name': 'Motorcycle'.tr}, // ترجمة: دراجة نارية
|
||||
{'id': 3, 'name': 'Van / Bus'.tr}, // ترجمة: فان / باص
|
||||
];
|
||||
|
||||
// قائمة أنواع الوقود
|
||||
final List<Map<String, dynamic>> fuelTypeOptions = [
|
||||
{'id': 1, 'name': 'Petrol'.tr}, // ترجمة: بنزين
|
||||
{'id': 2, 'name': 'Diesel'.tr}, // ترجمة: ديزل
|
||||
{'id': 3, 'name': 'Electric'.tr}, // ترجمة: كهربائي
|
||||
{'id': 4, 'name': 'Hybrid'.tr}, // ترجمة: هايبرد
|
||||
];
|
||||
// STEP 3: Document Uploads
|
||||
File? driverLicenseFrontImage;
|
||||
File? driverLicenseBackImage;
|
||||
File? carLicenseFrontImage;
|
||||
File? carLicenseBackImage;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
pageController = PageController();
|
||||
// Pre-fill phone number if it exists in storage
|
||||
// phoneController.text = box.read(BoxName.phoneDriver) ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
pageController.dispose();
|
||||
// Dispose all other text controllers
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// --- Page Navigation ---
|
||||
void goToNextStep() {
|
||||
bool isValid = false;
|
||||
if (currentPage.value == 0) {
|
||||
// Validate Step 1
|
||||
isValid = driverInfoFormKey.currentState!.validate();
|
||||
if (isValid) {
|
||||
// Optional: Check if license is expired
|
||||
// if (driverLicenseExpiryDate != null &&
|
||||
// driverLicenseExpiryDate!.isBefore(DateTime.now())) {
|
||||
// Get.snackbar('Expired License', 'Your driver’s license has expired.'.tr
|
||||
// ,
|
||||
// snackPosition: SnackPosition.BOTTOM,
|
||||
// backgroundColor: Colors.red,
|
||||
// colorText: Colors.white);
|
||||
// return; // Stop progression
|
||||
// }
|
||||
}
|
||||
} else if (currentPage.value == 1) {
|
||||
// Validate Step 2
|
||||
isValid = carInfoFormKey.currentState!.validate();
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void goToPreviousStep() {
|
||||
pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeIn,
|
||||
);
|
||||
}
|
||||
|
||||
// --- Image Picking ---
|
||||
Future<void> pickImage(ImageType type) async {
|
||||
try {
|
||||
final picker = ImagePicker();
|
||||
final picked = await picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 95, // جودة أولية من الكاميرا
|
||||
maxWidth: 3000, // نسمح بصورة كبيرة ثم نصغّر نحن
|
||||
);
|
||||
if (picked == null) return;
|
||||
|
||||
// قصّ الصورة
|
||||
final cropped = await ImageCropper().cropImage(
|
||||
sourcePath: picked.path,
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: 'Cropper'.tr,
|
||||
toolbarColor: AppColor.accentColor,
|
||||
toolbarWidgetColor: AppColor.redColor,
|
||||
initAspectRatio: CropAspectRatioPreset.original,
|
||||
lockAspectRatio: false,
|
||||
),
|
||||
IOSUiSettings(title: 'Cropper'.tr),
|
||||
],
|
||||
);
|
||||
if (cropped == null) return; // المستخدم ألغى
|
||||
|
||||
// قراءة bytes + التصحيح حسب EXIF ثم التصغير
|
||||
final rawBytes = await File(cropped.path).readAsBytes();
|
||||
final decoded = img.decodeImage(rawBytes);
|
||||
if (decoded == null) throw Exception('Decode image failed');
|
||||
|
||||
// تصحيح اتجاه الصورة (EXIF)
|
||||
final fixed = img.bakeOrientation(decoded);
|
||||
|
||||
// تصغير لعرض 800px (عدّل عند الحاجة)
|
||||
final resized = img.copyResize(fixed, width: 800);
|
||||
|
||||
// حفظ مؤقت بصيغة JPG
|
||||
final tmpDir = await getTemporaryDirectory();
|
||||
final outPath =
|
||||
'${tmpDir.path}/doc_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||
final outFile = File(outPath);
|
||||
await outFile.writeAsBytes(img.encodeJpg(resized, quality: 85));
|
||||
|
||||
// عيّن الملف في المتغير الصحيح حسب النوع
|
||||
if (outFile != null) {
|
||||
switch (type) {
|
||||
case ImageType.driverLicenseFront:
|
||||
driverLicenseFrontImage = File(outFile.path);
|
||||
break;
|
||||
case ImageType.driverLicenseBack:
|
||||
driverLicenseBackImage = File(outFile.path);
|
||||
break;
|
||||
case ImageType.carLicenseFront:
|
||||
carLicenseFrontImage = File(outFile.path);
|
||||
break;
|
||||
case ImageType.carLicenseBack:
|
||||
carLicenseBackImage = File(outFile.path);
|
||||
break;
|
||||
}
|
||||
update(); // Use update() to refresh the GetBuilder UI
|
||||
}
|
||||
|
||||
update(); // لتحديث الـ UI
|
||||
|
||||
// // الإرسال للذكاء الاصطناعي
|
||||
// await sendToAI(type, imageFile: outFile);
|
||||
} catch (e) {
|
||||
mySnackeBarError('${'An unexpected error occurred:'.tr} $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ثابت: 20 لون سيارة شائع
|
||||
static const List<Map<String, String>> kCarColorOptions = [
|
||||
{'key': 'color.white', 'hex': '#FFFFFF'},
|
||||
{'key': 'color.black', 'hex': '#000000'},
|
||||
{'key': 'color.silver', 'hex': '#C0C0C0'},
|
||||
{'key': 'color.gray', 'hex': '#808080'},
|
||||
{'key': 'color.gunmetal', 'hex': '#2A3439'},
|
||||
{'key': 'color.red', 'hex': '#C62828'},
|
||||
{'key': 'color.blue', 'hex': '#1565C0'},
|
||||
{'key': 'color.navy', 'hex': '#0D47A1'},
|
||||
{'key': 'color.green', 'hex': '#2E7D32'},
|
||||
{'key': 'color.darkGreen', 'hex': '#1B5E20'},
|
||||
{'key': 'color.beige', 'hex': '#D7CCC8'},
|
||||
{'key': 'color.brown', 'hex': '#5D4037'},
|
||||
{'key': 'color.maroon', 'hex': '#800000'},
|
||||
{'key': 'color.burgundy', 'hex': '#800020'},
|
||||
{'key': 'color.yellow', 'hex': '#F9A825'},
|
||||
{'key': 'color.orange', 'hex': '#EF6C00'},
|
||||
{'key': 'color.gold', 'hex': '#D4AF37'},
|
||||
{'key': 'color.bronze', 'hex': '#CD7F32'},
|
||||
{'key': 'color.champagne', 'hex': '#EFE1C6'},
|
||||
{'key': 'color.purple', 'hex': '#6A1B9A'},
|
||||
];
|
||||
|
||||
Color hexToColor(String hex) {
|
||||
var v = hex.replaceAll('#', '');
|
||||
if (v.length == 6) v = 'FF$v';
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// خريطة لتخزين روابط المستندات بعد الرفع
|
||||
final Map<String, String> docUrls = {
|
||||
'driver_license_front': '',
|
||||
'driver_license_back': '',
|
||||
'car_license_front': '',
|
||||
'car_license_back': '',
|
||||
};
|
||||
|
||||
/// التصرّف العام لاختيار/قص/ضغط/رفع الصورة حسب type
|
||||
Future<void> choosImage(String link, String imageType) async {
|
||||
try {
|
||||
final pickedImage = await picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
preferredCameraDevice: CameraDevice.rear,
|
||||
);
|
||||
if (pickedImage == null) return;
|
||||
|
||||
image = File(pickedImage.path);
|
||||
|
||||
final croppedFile = await ImageCropper().cropImage(
|
||||
sourcePath: image!.path,
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: 'Cropper'.tr,
|
||||
toolbarColor: AppColor.blueColor,
|
||||
toolbarWidgetColor: AppColor.yellowColor,
|
||||
initAspectRatio: CropAspectRatioPreset.original,
|
||||
lockAspectRatio: false,
|
||||
),
|
||||
IOSUiSettings(title: 'Cropper'.tr),
|
||||
],
|
||||
);
|
||||
if (croppedFile == null) return;
|
||||
|
||||
// صورة للمعاينة داخل التطبيق
|
||||
myImage = File(croppedFile.path);
|
||||
|
||||
isloading = true;
|
||||
update();
|
||||
|
||||
// ضغط (وأيضاً يمكنك إضافة rotateImageIfNeeded قبل/بعد الضغط إن رغبت)
|
||||
final File compressedImage = await compressImage(File(croppedFile.path));
|
||||
|
||||
// تجهيز الحقول
|
||||
final driverId = box.read(BoxName.driverID);
|
||||
|
||||
final payload = <String, String>{
|
||||
'driverID': driverId,
|
||||
'imageType': imageType, // مثال: driver_license_front
|
||||
};
|
||||
|
||||
// الرفع وإرجاع الرابط
|
||||
final String imageUrl = await uploadImage(compressedImage, payload, link);
|
||||
|
||||
// حفظ الرابط محلياً حسب النوع
|
||||
docUrls[imageType] = imageUrl;
|
||||
|
||||
Log.print('✅ Uploaded $imageType => $imageUrl');
|
||||
} catch (e, st) {
|
||||
Log.print('❌ Error in choosImage: $e\n$st');
|
||||
mySnackeBarError('Image Upload Failed'.tr);
|
||||
} finally {
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
/// ترفع الملف وترجع رابط الصورة النهائي كـ String
|
||||
Future<String> uploadImage(
|
||||
File file, Map<String, String> data, String link) async {
|
||||
final uri = Uri.parse(link);
|
||||
final request = http.MultipartRequest('POST', uri);
|
||||
|
||||
// الهيدرز (كما عندك)
|
||||
final headers = <String, String>{
|
||||
'Authorization':
|
||||
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}',
|
||||
'X-HMAC-Auth': '${box.read(BoxName.hmac)}',
|
||||
};
|
||||
request.headers.addAll(headers);
|
||||
|
||||
// اسم الملف: driverID.jpg (اختياري)
|
||||
final forcedName = '${box.read(BoxName.driverID) ?? 'image'}.jpg';
|
||||
|
||||
// إضافة الملف (من المسار مباشرة أسلم من الـ stream)
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath(
|
||||
'image', // تأكد أنه نفس اسم الحقل على السيرفر
|
||||
file.path,
|
||||
filename: forcedName,
|
||||
),
|
||||
);
|
||||
|
||||
// الحقول الإضافية
|
||||
data.forEach((k, v) => request.fields[k] = v);
|
||||
|
||||
// الإرسال
|
||||
final streamed = await request.send();
|
||||
final res = await http.Response.fromStream(streamed);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception(
|
||||
'Failed to upload image: ${res.statusCode} - ${res.body}');
|
||||
}
|
||||
|
||||
// نحاول استخراج رابط الصورة من أكثر من مفتاح محتمل
|
||||
final body = jsonDecode(res.body);
|
||||
final String? url = body['url'] ??
|
||||
body['file_link'] ??
|
||||
body['image_url'] ??
|
||||
(body['data'] is Map ? body['data']['url'] : null);
|
||||
|
||||
if (url == null || url.isEmpty) {
|
||||
// لو السيرفر يرجع هيكل مختلف، عدّل هنا المفتاح حسب استجابتك الفعلية
|
||||
throw Exception(
|
||||
'Upload succeeded but no image URL found in response: ${res.body}');
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
Future<File> compressImage(File file) async {
|
||||
final dir = await path_provider.getTemporaryDirectory();
|
||||
final targetPath = "${dir.absolute.path}/temp.jpg";
|
||||
|
||||
var result = await FlutterImageCompress.compressAndGetFile(
|
||||
file.absolute.path,
|
||||
targetPath,
|
||||
quality: 70,
|
||||
minWidth: 1024,
|
||||
minHeight: 1024,
|
||||
);
|
||||
|
||||
return File(result!.path);
|
||||
}
|
||||
|
||||
// دالة رفع إلى السيرفر السوري: ترجع 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 {
|
||||
// 0) دوال/مساعدات محلية
|
||||
void _addField(Map<String, String> fields, String key, String? value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
fields[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 1) تحقق من وجود الروابط
|
||||
final driverFrontUrl = docUrls['driver_license_front'];
|
||||
final driverBackUrl = docUrls['driver_license_back'];
|
||||
final carFrontUrl = docUrls['car_license_front'];
|
||||
final carBackUrl = docUrls['car_license_back'];
|
||||
Log.print(driverFrontUrl.toString());
|
||||
Log.print(driverBackUrl.toString());
|
||||
Log.print(carFrontUrl.toString());
|
||||
Log.print(carBackUrl.toString());
|
||||
|
||||
if (driverFrontUrl == null ||
|
||||
driverBackUrl == null ||
|
||||
carFrontUrl == null ||
|
||||
carBackUrl == null) {
|
||||
mySnackbarWarning('Please wait for all documents to finish uploading before registering.'.tr);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
update();
|
||||
|
||||
final registerUri = Uri.parse(AppLink.register_driver_and_car);
|
||||
final client = http.Client();
|
||||
|
||||
try {
|
||||
// ترويسات مشتركة
|
||||
final bearer =
|
||||
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}';
|
||||
final hmac = '${box.read(BoxName.hmac)}';
|
||||
|
||||
String fingerPrint =
|
||||
box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
||||
String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
|
||||
String nonce = timestamp; // Simple nonce for now
|
||||
|
||||
final req = http.MultipartRequest('POST', registerUri);
|
||||
req.headers.addAll({
|
||||
'Authorization': bearer,
|
||||
// 'X-HMAC-Auth': hmac, // Removed to bypass "Invalid HMAC signature" check
|
||||
'X-Device-FP': fingerPrint,
|
||||
'X-Timestamp': timestamp,
|
||||
'X-Nonce': nonce,
|
||||
});
|
||||
|
||||
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, 'birthdate', bithdateController.text);
|
||||
_addField(fields, 'expiry_date', driverLicenseExpiryController.text);
|
||||
_addField(fields, 'password', 'generated_password_or_token');
|
||||
_addField(fields, 'status', 'yet');
|
||||
_addField(fields, 'email', 'Not specified');
|
||||
_addField(fields, 'gender', 'Male'); // يفضل ربطها بـ Dropdown أيضاً
|
||||
|
||||
// --- 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',
|
||||
driverLicenseExpiryController
|
||||
.text); // تأكد من أن هذا تاريخ انتهاء السيارة وليس الرخصة
|
||||
_addField(
|
||||
fields,
|
||||
'color',
|
||||
carColorController.text.isNotEmpty
|
||||
? carColorController.text
|
||||
: 'White');
|
||||
|
||||
_addField(fields, 'color_hex',
|
||||
(colorHex != null && colorHex!.isNotEmpty) ? colorHex! : '#FFFFFF');
|
||||
|
||||
_addField(
|
||||
fields,
|
||||
'owner',
|
||||
'${firstNameController.text} ${lastNameController.text}'
|
||||
.trim()
|
||||
.isNotEmpty
|
||||
? '${firstNameController.text} ${lastNameController.text}'
|
||||
: 'Driver Owner');
|
||||
|
||||
// ============================================================
|
||||
// 🔥 التعديل الجديد: إرسال الأرقام (IDs) لتصنيف المركبة والوقود
|
||||
// ============================================================
|
||||
|
||||
// 1. إرسال رقم تصنيف المركبة (1=سيارة, 2=دراجة...)
|
||||
if (selectedVehicleCategoryId != null) {
|
||||
_addField(fields, 'vehicle_category_id',
|
||||
selectedVehicleCategoryId.toString());
|
||||
} else {
|
||||
_addField(fields, 'vehicle_category_id', '1'); // قيمة افتراضية (سيارة)
|
||||
}
|
||||
|
||||
// 2. إرسال رقم ونوع الوقود
|
||||
if (selectedFuelTypeId != null) {
|
||||
// إرسال الرقم (للبحث السريع)
|
||||
_addField(fields, 'fuel_type_id', selectedFuelTypeId.toString());
|
||||
|
||||
// إرسال الاسم نصاً (للتوافق مع العمود القديم 'fuel' إذا لزم الأمر)
|
||||
// نبحث عن الاسم داخل القائمة بناءً على الرقم المختار
|
||||
final fuelObj = fuelTypeOptions.firstWhere(
|
||||
(e) => e['id'] == selectedFuelTypeId,
|
||||
orElse: () => {'name': 'Petrol'});
|
||||
_addField(fields, 'fuel', fuelObj['name'].toString());
|
||||
} else {
|
||||
_addField(fields, 'fuel_type_id', '1');
|
||||
_addField(fields, 'fuel', 'Petrol');
|
||||
}
|
||||
|
||||
// --- روابط الصور ---
|
||||
_addField(fields, 'driver_license_front', driverFrontUrl!);
|
||||
_addField(fields, 'driver_license_back', driverBackUrl!);
|
||||
_addField(fields, 'car_license_front', carFrontUrl!);
|
||||
_addField(fields, 'car_license_back', carBackUrl!);
|
||||
|
||||
req.fields.addAll(fields);
|
||||
|
||||
// 3) الإرسال
|
||||
final streamed =
|
||||
await client.send(req).timeout(const Duration(seconds: 60));
|
||||
final resp = await http.Response.fromStream(streamed);
|
||||
|
||||
// 4) معالجة الاستجابة
|
||||
Map<String, dynamic>? json;
|
||||
try {
|
||||
Log.print('--- Registration Response: ${resp.body} ---');
|
||||
json = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||
} catch (_) {}
|
||||
|
||||
if (resp.statusCode == 200 && json?['status'] == 'success') {
|
||||
mySnackbarSuccess('Registration completed successfully!'.tr);
|
||||
|
||||
// منطق التوكن والإشعارات وتسجيل الدخول...
|
||||
final email = box.read(BoxName.emailDriver);
|
||||
final driverID = box.read(BoxName.driverID);
|
||||
|
||||
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||
await CRUD().post(link: AppLink.addTokensDriver, payload: {
|
||||
'captain_id': (box.read(BoxName.driverID)).toString(),
|
||||
'token': (box.read(BoxName.tokenDriver)).toString(),
|
||||
'fingerPrint': fingerPrint.toString(),
|
||||
});
|
||||
|
||||
NotificationService.sendNotification(
|
||||
target: 'service',
|
||||
title: 'New Driver Registration',
|
||||
body: 'Driver $driverID has submitted registration.',
|
||||
isTopic: true,
|
||||
category: 'new_service_request',
|
||||
);
|
||||
|
||||
final c = Get.isRegistered<LoginDriverController>()
|
||||
? Get.find<LoginDriverController>()
|
||||
: Get.put(LoginDriverController());
|
||||
c.loginWithGoogleCredential(driverID, email);
|
||||
} else {
|
||||
final msg = (json?['message'] ?? 'Registration failed.').toString();
|
||||
mySnackeBarError(msg);
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError('Error: $e');
|
||||
} finally {
|
||||
client.close();
|
||||
isLoading.value = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
// // 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
|
||||
}
|
||||
38
siro_driver/lib/controller/auth/tokens_controller.dart
Executable file
38
siro_driver/lib/controller/auth/tokens_controller.dart
Executable file
@@ -0,0 +1,38 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../main.dart';
|
||||
|
||||
class TokenController extends GetxController {
|
||||
bool isloading = false;
|
||||
|
||||
Future addToken() async {
|
||||
String? basicAuthCredentials =
|
||||
await storage.read(key: BoxName.basicAuthCredentials);
|
||||
isloading = true;
|
||||
update();
|
||||
var res = await http.post(
|
||||
Uri.parse(AppLink.addTokens),
|
||||
headers: {
|
||||
'Authorization':
|
||||
'Basic ${base64Encode(utf8.encode(basicAuthCredentials.toString()))}',
|
||||
},
|
||||
body: {
|
||||
'token': box.read(BoxName.tokenFCM.toString()),
|
||||
'passengerID': box.read(BoxName.passengerID).toString()
|
||||
},
|
||||
);
|
||||
|
||||
isloading = false;
|
||||
update();
|
||||
var jsonToken = jsonDecode(res.body);
|
||||
if (jsonToken['status'] == 'The token has been updated successfully.') {
|
||||
mySnackbarSuccess('token updated'.tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
siro_driver/lib/controller/auth/verify_email_controller.dart
Executable file
16
siro_driver/lib/controller/auth/verify_email_controller.dart
Executable file
@@ -0,0 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
|
||||
class VerifyEmailController extends GetxController {
|
||||
TextEditingController verfyCode = TextEditingController();
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
sendverfications() async {
|
||||
await CRUD().post(link: AppLink.sendVerifyEmail);
|
||||
}
|
||||
}
|
||||
53
siro_driver/lib/controller/firebase/access_token.dart
Executable file
53
siro_driver/lib/controller/firebase/access_token.dart
Executable file
@@ -0,0 +1,53 @@
|
||||
import 'dart:convert';
|
||||
import 'package:googleapis_auth/auth_io.dart';
|
||||
|
||||
import '../../print.dart';
|
||||
|
||||
class AccessTokenManager {
|
||||
static final AccessTokenManager _instance = AccessTokenManager._internal();
|
||||
late final String serviceAccountJsonKey;
|
||||
AccessToken? _accessToken;
|
||||
DateTime? _expiryDate;
|
||||
|
||||
AccessTokenManager._internal();
|
||||
|
||||
factory AccessTokenManager(String jsonKey) {
|
||||
if (_instance._isServiceAccountKeyInitialized()) {
|
||||
// Prevent re-initialization
|
||||
return _instance;
|
||||
}
|
||||
_instance.serviceAccountJsonKey = jsonKey;
|
||||
return _instance;
|
||||
}
|
||||
|
||||
bool _isServiceAccountKeyInitialized() {
|
||||
try {
|
||||
serviceAccountJsonKey; // Access to check if initialized
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getAccessToken() async {
|
||||
if (_accessToken != null && DateTime.now().isBefore(_expiryDate!)) {
|
||||
return _accessToken!.data;
|
||||
}
|
||||
try {
|
||||
final serviceAccountCredentials = ServiceAccountCredentials.fromJson(
|
||||
json.decode(serviceAccountJsonKey));
|
||||
final client = await clientViaServiceAccount(
|
||||
serviceAccountCredentials,
|
||||
['https://www.googleapis.com/auth/firebase.messaging'],
|
||||
);
|
||||
|
||||
_accessToken = client.credentials.accessToken;
|
||||
_expiryDate = client.credentials.accessToken.expiry;
|
||||
client.close();
|
||||
// Log.print('_accessToken!.data: ${_accessToken!.data}');
|
||||
return _accessToken!.data;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to obtain access token');
|
||||
}
|
||||
}
|
||||
}
|
||||
18
siro_driver/lib/controller/firebase/bring_app_foreground.dart
Executable file
18
siro_driver/lib/controller/firebase/bring_app_foreground.dart
Executable file
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class AppLifecycleManager {
|
||||
static const platform = MethodChannel('com.sefer_driver/app_lifecycle');
|
||||
|
||||
static Future<void> bringAppToForeground() async {
|
||||
try {
|
||||
debugPrint('Attempting to bring app to foreground');
|
||||
await platform.invokeMethod('bringAppToForeground');
|
||||
debugPrint('Method invocation completed');
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint("Failed to bring app to foreground: '${e.message}'.");
|
||||
} catch (e) {
|
||||
debugPrint("Unexpected error: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
373
siro_driver/lib/controller/firebase/firbase_messge.dart
Executable file
373
siro_driver/lib/controller/firebase/firbase_messge.dart
Executable file
@@ -0,0 +1,373 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:siro_driver/controller/home/captin/home_captain_controller.dart';
|
||||
import 'package:siro_driver/views/home/Captin/orderCaptin/order_speed_request.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:siro_driver/controller/voice_call_controller.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../../views/auth/captin/criminal_documents_page.dart';
|
||||
import '../../views/home/Captin/home_captain/home_captin.dart';
|
||||
import '../../views/home/Captin/orderCaptin/order_request_page.dart';
|
||||
import '../../views/home/Captin/orderCaptin/vip_order_page.dart';
|
||||
import '../auth/google_sign.dart';
|
||||
import '../functions/face_detect.dart';
|
||||
import '../home/captin/map_driver_controller.dart';
|
||||
import 'local_notification.dart';
|
||||
|
||||
class FirebaseMessagesController extends GetxController {
|
||||
final fcmToken = FirebaseMessaging.instance;
|
||||
|
||||
List<String> tokens = [];
|
||||
List dataTokens = [];
|
||||
late String driverID;
|
||||
late String driverToken;
|
||||
NotificationSettings? notificationSettings;
|
||||
NotificationController notificationController =
|
||||
Get.put(NotificationController());
|
||||
Future<void> getNotificationSettings() async {
|
||||
// Get the current notification settings
|
||||
NotificationSettings? notificationSettings =
|
||||
await FirebaseMessaging.instance.getNotificationSettings();
|
||||
'Notification authorization status: ${notificationSettings.authorizationStatus}';
|
||||
|
||||
// Call the update function if needed
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> requestFirebaseMessagingPermission() async {
|
||||
FirebaseMessaging messaging = FirebaseMessaging.instance;
|
||||
|
||||
// Check if the platform is Android
|
||||
if (Platform.isAndroid) {
|
||||
// Request permission for Android
|
||||
await messaging.requestPermission();
|
||||
} else if (Platform.isIOS) {
|
||||
// Request permission for iOS
|
||||
NotificationSettings settings = await messaging.requestPermission(
|
||||
alert: true,
|
||||
announcement: true,
|
||||
badge: true,
|
||||
carPlay: true,
|
||||
criticalAlert: true,
|
||||
provisional: false,
|
||||
sound: true,
|
||||
);
|
||||
messaging.setForegroundNotificationPresentationOptions(
|
||||
alert: true, badge: true, sound: true);
|
||||
}
|
||||
}
|
||||
|
||||
Future getToken() async {
|
||||
fcmToken.getToken().then((token) {
|
||||
Log.print('token fcm driver: ${token}');
|
||||
box.write(BoxName.tokenDriver, (token!));
|
||||
});
|
||||
// 🔹 الاشتراك في topic
|
||||
await fcmToken.subscribeToTopic("drivers"); // أو "users" حسب نوع المستخدم
|
||||
print("Subscribed to 'drivers' topic ✅");
|
||||
|
||||
FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage? message) async {
|
||||
if (message != null && message.data.isNotEmpty) {
|
||||
Log.print("🔔 FCM getInitialMessage payload: ${message.data}");
|
||||
String? category = message.data['category'] ?? message.data['type'];
|
||||
if (category == 'ORDER' || category == 'Order' || category == 'OrderVIP' || message.data.containsKey('DriverList')) {
|
||||
String? myListString = message.data['DriverList'];
|
||||
if (myListString != null && myListString.isNotEmpty) {
|
||||
await storage.write(key: 'pending_driver_list', value: myListString);
|
||||
Log.print("💾 Saved pending driver list to secure storage from getInitialMessage");
|
||||
}
|
||||
} else {
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
fireBaseTitles(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
||||
// If the app is in the background or terminated, show a system tray message
|
||||
RemoteNotification? notification = message.notification;
|
||||
AndroidNotification? android = notification?.android;
|
||||
// if (notification != null && android != null) {
|
||||
|
||||
if (message.data.isNotEmpty) {
|
||||
fireBaseTitles(message);
|
||||
}
|
||||
// if (message.data.isNotEmpty && message.notification != null) {
|
||||
// fireBaseTitles(message);
|
||||
// }
|
||||
});
|
||||
FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {});
|
||||
|
||||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
||||
if (message.data.isNotEmpty) {
|
||||
fireBaseTitles(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fireBaseTitles(RemoteMessage message) async {
|
||||
// [!! تعديل جوهري !!]
|
||||
// اقرأ "النوع" من حمولة البيانات، وليس من العنوان
|
||||
String category = message.data['category'] ?? '';
|
||||
|
||||
// اقرأ العنوان والنص (للعرض)
|
||||
String title = message.notification?.title ?? '';
|
||||
String body = message.notification?.body ?? '';
|
||||
|
||||
// استخدم switch لسهولة القراءة والصيانة
|
||||
switch (category) {
|
||||
case 'ORDER':
|
||||
case 'Order': // Handle both cases for backward compatibility
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(title, body, 'order', '');
|
||||
// }
|
||||
|
||||
// 🔥 [Fix FCM-Guard] منع إعاقة الرحلة النشطة بطلبات جديدة عبر FCM
|
||||
String currentRideStatus = box.read(BoxName.rideStatus) ?? '';
|
||||
if (currentRideStatus == 'Begin' ||
|
||||
currentRideStatus == 'Apply' ||
|
||||
currentRideStatus == 'Arrived') {
|
||||
Log.print(
|
||||
"⛔ [FCM] Ignoring ORDER notification — driver has active ride ($currentRideStatus)");
|
||||
break;
|
||||
}
|
||||
|
||||
var myListString = message.data['DriverList'];
|
||||
if (myListString != null) {
|
||||
var myList = jsonDecode(myListString) as List<dynamic>;
|
||||
driverToken = myList[14].toString();
|
||||
Get.put(HomeCaptainController(), permanent: true).changeRideId();
|
||||
update();
|
||||
Get.toNamed('/OrderRequestPage', arguments: {
|
||||
'myListString': myListString,
|
||||
'DriverList': myList,
|
||||
'body': body
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'OrderVIP':
|
||||
var myListString = message.data['DriverList'];
|
||||
if (myListString != null) {
|
||||
var myList = jsonDecode(myListString) as List<dynamic>;
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'order', '');
|
||||
}
|
||||
Get.to(VipOrderPage(), arguments: {
|
||||
'myListString': myListString,
|
||||
'DriverList': myList,
|
||||
'body': body
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Cancel Trip':
|
||||
case 'TRIP_CANCELLED':
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(
|
||||
title, 'Passenger Cancel Trip'.tr, 'cancel', '');
|
||||
}
|
||||
Log.print("🔔 FCM: Ride Cancelled by Passenger received.");
|
||||
|
||||
// 1. استخراج السبب (أرسلناه من PHP باسم 'reason')
|
||||
String reason = message.data['reason'] ?? 'No reason provided';
|
||||
|
||||
// 2. توجيه الأمر للكنترولر
|
||||
if (Get.isRegistered<MapDriverController>()) {
|
||||
// استدعاء الحارس (سيتجاهل الأمر إذا كان السوكيت قد سبقه)
|
||||
Get.find<MapDriverController>()
|
||||
.processRideCancelledByPassenger(reason, source: "FCM");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'VIP Order Accepted':
|
||||
// This seems to be a notification for the passenger, but if the driver needs to see it:
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'order', '');
|
||||
}
|
||||
// Maybe show a simple snackbar confirmation
|
||||
mySnackbarSuccess('You accepted the VIP order.'.tr);
|
||||
break;
|
||||
|
||||
case 'message From passenger':
|
||||
case 'MSG_FROM_PASSENGER':
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'ding', '');
|
||||
}
|
||||
MyDialog().getDialog(title, body, () {
|
||||
// Empty callback, MyDialog already closes itself using pop().
|
||||
});
|
||||
break;
|
||||
|
||||
case 'token change':
|
||||
case 'TOKEN_CHANGE':
|
||||
GoogleSignInHelper.signOut();
|
||||
break;
|
||||
|
||||
case 'face detect':
|
||||
case 'FACE_DETECT':
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'tone2', '');
|
||||
}
|
||||
String result0 = await faceDetector();
|
||||
var result = jsonDecode(result0);
|
||||
MyDialogContent().getDialog(
|
||||
'Face Detection Result'.tr,
|
||||
Text(
|
||||
result['similar'].toString() == 'true'
|
||||
? 'similar'.tr
|
||||
: 'not similar'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
() {
|
||||
// Navigator.pop(Get.context!);
|
||||
},
|
||||
);
|
||||
update();
|
||||
break;
|
||||
|
||||
case 'Hi ,I will go now':
|
||||
case 'PASSENGER_COMING':
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'tone2', '');
|
||||
}
|
||||
update();
|
||||
break;
|
||||
|
||||
case 'Criminal Document Required':
|
||||
case 'DOC_REQUIRED':
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'tone2', '');
|
||||
}
|
||||
MyDialog().getDialog(title, 'You should have upload it .'.tr, () {
|
||||
Get.to(() => const CriminalDocumemtPage());
|
||||
});
|
||||
break;
|
||||
|
||||
case 'Order Applied':
|
||||
case 'ORDER_TAKEN':
|
||||
mySnackbarSuccess("The order has been accepted by another driver.".tr);
|
||||
break;
|
||||
|
||||
case 'incoming_call':
|
||||
case 'INCOMING_CALL':
|
||||
final sessionId = message.data['session_id'];
|
||||
final callerName = message.data['caller_name'];
|
||||
final rideId = message.data['ride_id'];
|
||||
if (sessionId != null && callerName != null && rideId != null) {
|
||||
Get.find<VoiceCallController>().receiveCall(
|
||||
sessionIdVal: sessionId.toString(),
|
||||
remoteNameVal: callerName.toString(),
|
||||
rideIdVal: rideId.toString(),
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.print('Received unhandled notification category: $category');
|
||||
// Optionally show a generic notification
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'default', '');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SnackbarController driverAppliedTripSnakBar() {
|
||||
return Get.snackbar(
|
||||
'Driver Applied the Ride for You'.tr,
|
||||
'',
|
||||
colorText: AppColor.greenColor,
|
||||
duration: const Duration(seconds: 3),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
titleText: Text(
|
||||
'Applied'.tr,
|
||||
style: const TextStyle(color: AppColor.redColor),
|
||||
),
|
||||
messageText: Text(
|
||||
'Driver Applied the Ride for You'.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
icon: const Icon(Icons.approval),
|
||||
shouldIconPulse: true,
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
);
|
||||
}
|
||||
|
||||
Future<dynamic> cancelTripDialog() {
|
||||
return Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'Passenger Cancel Trip'.tr,
|
||||
middleText: '',
|
||||
confirm: MyElevatedButton(
|
||||
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());
|
||||
}));
|
||||
}
|
||||
|
||||
Future<dynamic> cancelTripDialog1() {
|
||||
return Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'Passenger Cancel Trip'.tr,
|
||||
middleText:
|
||||
'Trip Cancelled. The cost of the trip will be added to your wallet.'
|
||||
.tr,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () {
|
||||
box.write(BoxName.rideStatus, 'Cancel');
|
||||
Log.print(
|
||||
'rideStatus from 364 : ${box.read(BoxName.rideStatus)}');
|
||||
Get.offAll(HomeCaptain());
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
class OverlayContent extends StatelessWidget {
|
||||
final String title;
|
||||
final String body;
|
||||
|
||||
OverlayContent(this.title, this.body);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
body,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
548
siro_driver/lib/controller/firebase/local_notification.dart
Executable file
548
siro_driver/lib/controller/firebase/local_notification.dart
Executable file
@@ -0,0 +1,548 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:ui'; // للألوان
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:timezone/data/latest.dart' as tz;
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../main.dart'; // للوصول لـ box
|
||||
import '../../print.dart';
|
||||
import '../../views/home/Captin/driver_map_page.dart';
|
||||
import '../../views/home/Captin/orderCaptin/order_request_page.dart';
|
||||
import '../functions/crud.dart';
|
||||
import '../home/captin/home_captain_controller.dart';
|
||||
|
||||
class NotificationController extends GetxController {
|
||||
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
// ==============================================================================
|
||||
// 1. تهيئة الإشعارات (إعداد القنوات والأزرار للآيفون والأندرويد)
|
||||
// ==============================================================================
|
||||
Future<void> initNotifications() async {
|
||||
// إعدادات الأندرويد
|
||||
const AndroidInitializationSettings android =
|
||||
AndroidInitializationSettings('@mipmap/launcher_icon');
|
||||
|
||||
// إعدادات أزرار الآيفون (Categories)
|
||||
// هذا الجزء ضروري لظهور الأزرار في iOS
|
||||
final List<DarwinNotificationCategory> darwinNotificationCategories = [
|
||||
DarwinNotificationCategory(
|
||||
'ORDER_CATEGORY', // المعرف المستخدم لربط الإشعار بالأزرار
|
||||
actions: [
|
||||
DarwinNotificationAction.plain('ACCEPT_ORDER', '✅ قبول'),
|
||||
DarwinNotificationAction.plain('SHOW_DETAILS', '📄 تفاصيل'),
|
||||
DarwinNotificationAction.plain(
|
||||
'REJECT_ORDER',
|
||||
'❌ رفض',
|
||||
options: {
|
||||
DarwinNotificationActionOption.destructive
|
||||
}, // يظهر باللون الأحمر
|
||||
),
|
||||
],
|
||||
)
|
||||
];
|
||||
|
||||
// إعدادات الآيفون العامة
|
||||
final DarwinInitializationSettings ios = DarwinInitializationSettings(
|
||||
requestAlertPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
notificationCategories: darwinNotificationCategories, // تسجيل الأزرار
|
||||
);
|
||||
|
||||
InitializationSettings initializationSettings =
|
||||
InitializationSettings(android: android, iOS: ios);
|
||||
|
||||
tz.initializeTimeZones();
|
||||
print('✅ Notifications initialized with Action Buttons Support');
|
||||
|
||||
await _flutterLocalNotificationsPlugin.initialize(
|
||||
onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
|
||||
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
|
||||
settings: initializationSettings,
|
||||
);
|
||||
|
||||
// إنشاء قناة الأندرويد ذات الأهمية القصوى
|
||||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||
'high_importance_channel',
|
||||
'High Importance Notifications',
|
||||
description: 'This channel is used for important notifications.',
|
||||
importance: Importance.max, // أقصى أهمية
|
||||
playSound: true,
|
||||
);
|
||||
|
||||
await _flutterLocalNotificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// 2. دالة عرض الإشعار المطور (شكل واضح + أزرار + صوت مخصص)
|
||||
// ==============================================================================
|
||||
void showOrderNotification(
|
||||
String title, String body, String tone, String myListString) async {
|
||||
// أ) تنسيق النص والبيانات بشكل جميل
|
||||
String formattedBigText = body;
|
||||
String summaryText = 'طلب جديد';
|
||||
String price = '';
|
||||
|
||||
try {
|
||||
List<dynamic> data = jsonDecode(myListString);
|
||||
// استخراج البيانات (تأكد أن الاندكسات مطابقة للباك إند عندك)
|
||||
price = _getVal(data, 26);
|
||||
String distance = _getVal(data, 5);
|
||||
String startLoc = _getVal(data, 29);
|
||||
String endLoc = _getVal(data, 30);
|
||||
String paxName = _getVal(data, 8);
|
||||
// String rating = _getVal(data, 33);
|
||||
String isHaveSteps = _getVal(data, 20);
|
||||
|
||||
// تنسيق النص ليكون 4 أسطر واضحة
|
||||
formattedBigText = "👤 $paxName\n"
|
||||
"💰 $price ${'SYP'.tr} | 🛣️ $distance كم\n"
|
||||
"🟢 من: $startLoc\n"
|
||||
"🏁 إلى: $endLoc";
|
||||
|
||||
if (isHaveSteps == 'true') {
|
||||
formattedBigText += "\n🛑 هذه الرحلة تحتوي على نقاط توقف!";
|
||||
}
|
||||
|
||||
summaryText = 'سعر الرحلة: $price';
|
||||
} catch (e) {
|
||||
print("Error formatting notification text: $e");
|
||||
}
|
||||
|
||||
// ب) نمط النص الكبير (BigText) للأندرويد
|
||||
BigTextStyleInformation bigTextStyleInformation = BigTextStyleInformation(
|
||||
formattedBigText,
|
||||
contentTitle: '🚖 $title',
|
||||
summaryText: summaryText,
|
||||
htmlFormatContent: true,
|
||||
htmlFormatContentTitle: true,
|
||||
);
|
||||
|
||||
// ج) معالجة اسم الصوت (أندرويد بدون امتداد، آيفون مع امتداد)
|
||||
String soundNameAndroid = tone.contains('.') ? tone.split('.').first : tone;
|
||||
String soundNameIOS = tone.contains('.') ? tone : "$tone.wav";
|
||||
|
||||
// د) إعدادات الأندرويد (الأزرار + Full Screen)
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
'high_importance_channel',
|
||||
'High Importance Notifications',
|
||||
importance: Importance.max,
|
||||
priority: Priority.max,
|
||||
fullScreenIntent: true, // يفتح الشاشة وتظهر التفاصيل
|
||||
category: AndroidNotificationCategory.call, // يعامل كمكالمة (رنين مستمر)
|
||||
visibility: NotificationVisibility.public,
|
||||
ongoing: true, // يمنع الحذف بالسحب
|
||||
sound: RawResourceAndroidNotificationSound(soundNameAndroid),
|
||||
audioAttributesUsage: AudioAttributesUsage.alarm, // صوت عالٍ كالمنبه
|
||||
styleInformation: bigTextStyleInformation,
|
||||
color: const Color(0xFF1A252F),
|
||||
|
||||
// الأزرار الثلاثة
|
||||
actions: <AndroidNotificationAction>[
|
||||
const AndroidNotificationAction(
|
||||
'ACCEPT_ORDER',
|
||||
'✅ قبول فوري',
|
||||
showsUserInterface: true,
|
||||
titleColor: Color(0xFF4CAF50), // أخضر
|
||||
),
|
||||
const AndroidNotificationAction(
|
||||
'SHOW_DETAILS',
|
||||
'📄 التفاصيل',
|
||||
showsUserInterface: true,
|
||||
titleColor: Color(0xFF2196F3), // أزرق
|
||||
),
|
||||
const AndroidNotificationAction(
|
||||
'REJECT_ORDER',
|
||||
'❌ رفض',
|
||||
showsUserInterface: false, // لا يفتح التطبيق
|
||||
cancelNotification: true,
|
||||
titleColor: Color(0xFFE53935), // أحمر
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// هـ) إعدادات الآيفون
|
||||
final iosDetails = DarwinNotificationDetails(
|
||||
sound: soundNameIOS,
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
categoryIdentifier: 'ORDER_CATEGORY', // ربط الأزرار
|
||||
interruptionLevel: InterruptionLevel.critical, // محاولة لكسر الصامت
|
||||
);
|
||||
|
||||
final details =
|
||||
NotificationDetails(android: androidDetails, iOS: iosDetails);
|
||||
|
||||
String briefBody = "$price - مسافة $formattedBigText";
|
||||
if (_getVal(jsonDecode(myListString), 20) == 'true') {
|
||||
briefBody = "🛑 (متعددة التوقفات) $price - مسافة $formattedBigText";
|
||||
}
|
||||
|
||||
// عرض الإشعار
|
||||
await _flutterLocalNotificationsPlugin.show(
|
||||
id: 1001, // ID ثابت لاستبدال الإشعار القديم
|
||||
title: title,
|
||||
body: briefBody, // نص مختصر يظهر في البار العلوي
|
||||
notificationDetails: details,
|
||||
payload: jsonEncode({
|
||||
'type': 'Order',
|
||||
'data': myListString,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// 3. معالجة الاستجابة (عند الضغط على الأزرار)
|
||||
// ==============================================================================
|
||||
Future<void> handleNotificationResponse(NotificationResponse response) async {
|
||||
final payload = response.payload;
|
||||
if (payload == null) return;
|
||||
|
||||
final payloadData = jsonDecode(payload) as Map<String, dynamic>;
|
||||
final rawData = payloadData['data'];
|
||||
|
||||
List<dynamic> listData = [];
|
||||
if (rawData is String) {
|
||||
listData = jsonDecode(rawData);
|
||||
} else if (rawData is List) {
|
||||
listData = rawData;
|
||||
}
|
||||
|
||||
print("🔔 Notification Action: ${response.actionId}");
|
||||
|
||||
// أ) زر القبول
|
||||
if (response.actionId == 'ACCEPT_ORDER') {
|
||||
await _flutterLocalNotificationsPlugin.cancel(id: 1001); // حذف الإشعار
|
||||
_processAcceptOrder(listData);
|
||||
}
|
||||
|
||||
// ب) زر التفاصيل
|
||||
else if (response.actionId == 'SHOW_DETAILS') {
|
||||
// await _flutterLocalNotificationsPlugin.cancel(1001); // اختياري: حذف الإشعار
|
||||
Get.to(() => OrderRequestPage(), arguments: {'myListString': rawData});
|
||||
}
|
||||
|
||||
// ج) زر الرفض
|
||||
else if (response.actionId == 'REJECT_ORDER') {
|
||||
await _flutterLocalNotificationsPlugin.cancel(id: 1001); // حذف الإشعار
|
||||
_processRejectOrder(listData);
|
||||
}
|
||||
|
||||
// د) الضغط على الإشعار نفسه (بدون أزرار)
|
||||
else {
|
||||
Get.to(() => OrderRequestPage(), arguments: {'myListString': rawData});
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// 4. منطق القبول الآمن (Safe Accept Logic)
|
||||
// ==============================================================================
|
||||
Future<void> _processAcceptOrder(List<dynamic> data) async {
|
||||
// إظهار Loading
|
||||
Get.dialog(
|
||||
WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(color: Colors.white),
|
||||
),
|
||||
),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
|
||||
try {
|
||||
final driverId = box.read(BoxName.driverID);
|
||||
String orderId = _getVal(data, 16);
|
||||
String passengerToken = _getVal(data, 9);
|
||||
|
||||
print("🚀 Sending Accept Request for Order: $orderId");
|
||||
|
||||
var res = await CRUD().post(
|
||||
link: "${AppLink.ride}/rides/acceptRide.php",
|
||||
payload: {
|
||||
'id': orderId,
|
||||
'rideTimeStart': DateTime.now().toString(),
|
||||
'status': 'Apply',
|
||||
'passengerToken': passengerToken,
|
||||
'driver_id': driverId,
|
||||
},
|
||||
);
|
||||
|
||||
print("📥 Server Response: $res");
|
||||
|
||||
if (Get.isDialogOpen == true) Get.back(); // إغلاق اللودينج
|
||||
|
||||
// 🔴 فحص النتيجة بدقة (Map أو String)
|
||||
bool isFailure = false;
|
||||
if (res is Map && res['status'] == 'failure') {
|
||||
isFailure = true;
|
||||
} else if (res == 'failure') {
|
||||
isFailure = true;
|
||||
}
|
||||
|
||||
if (isFailure) {
|
||||
Get.defaultDialog(
|
||||
title: "تنبيه",
|
||||
middleText: "عذراً، الطلب أخذه سائق آخر.",
|
||||
confirmTextColor: Colors.white,
|
||||
onConfirm: () => Get.back(),
|
||||
textConfirm: "حسناً",
|
||||
);
|
||||
return; // توقف هنا ولا تكمل
|
||||
}
|
||||
|
||||
// ✅ نجاح -> تجهيز الانتقال
|
||||
|
||||
// حماية من الكراش: التأكد من وجود HomeCaptainController
|
||||
if (!Get.isRegistered<HomeCaptainController>()) {
|
||||
print("♻️ Reviving HomeCaptainController...");
|
||||
Get.put(HomeCaptainController(), permanent: true);
|
||||
} else {
|
||||
Get.find<HomeCaptainController>().changeRideId();
|
||||
}
|
||||
|
||||
box.write(BoxName.statusDriverLocation, 'on');
|
||||
box.write(BoxName.rideStatus, 'Apply');
|
||||
|
||||
var rideArgs = _buildRideArgs(data);
|
||||
box.write(BoxName.rideArguments, rideArgs);
|
||||
|
||||
// استخدام offAll لمنع الرجوع لصفحة الطلب
|
||||
Get.offAll(() => PassengerLocationMapPage(), arguments: rideArgs);
|
||||
} catch (e) {
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
print("❌ Error in accept process: $e");
|
||||
Get.snackbar("خطأ", "حدث خطأ غير متوقع");
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// 5. منطق الرفض (يعمل في الخلفية بدون فتح صفحات)
|
||||
// ==============================================================================
|
||||
Future<void> _processRejectOrder(List<dynamic> data) async {
|
||||
try {
|
||||
final driverId = box.read(BoxName.driverID);
|
||||
String orderId = _getVal(data, 16);
|
||||
|
||||
if (driverId != null && orderId.isNotEmpty) {
|
||||
print("📤 Rejecting Order: $orderId");
|
||||
await CRUD().post(link: AppLink.addDriverOrder, payload: {
|
||||
'driver_id': driverId,
|
||||
'order_id': orderId,
|
||||
'status': 'Refused'
|
||||
});
|
||||
print("✅ Order Rejected Successfully");
|
||||
}
|
||||
} catch (e) {
|
||||
print("❌ Error rejecting order: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// 6. دوال مساعدة (Helpers)
|
||||
// ==============================================================================
|
||||
|
||||
Map<String, dynamic> _buildRideArgs(List<dynamic> data) {
|
||||
return {
|
||||
'passengerLocation': '${_getVal(data, 0)},${_getVal(data, 1)}',
|
||||
'passengerDestination': '${_getVal(data, 3)},${_getVal(data, 4)}',
|
||||
'Duration': _getVal(data, 4), // انتبه: تأكد من الإندكس الصحيح للوقت
|
||||
'totalCost': _getVal(data, 26),
|
||||
'Distance': _getVal(data, 5),
|
||||
'name': _getVal(data, 8),
|
||||
'phone': _getVal(data, 10),
|
||||
'email': _getVal(data, 28),
|
||||
'WalletChecked': _getVal(data, 13),
|
||||
'tokenPassenger': _getVal(data, 9),
|
||||
'direction':
|
||||
'https://www.google.com/maps/dir/${_getVal(data, 0)}/${_getVal(data, 1)}/',
|
||||
'DurationToPassenger': _getVal(data, 15),
|
||||
'rideId': _getVal(data, 16),
|
||||
'passengerId': _getVal(data, 7),
|
||||
'driverId': _getVal(data, 18),
|
||||
'durationOfRideValue': _getVal(data, 19),
|
||||
'paymentAmount': _getVal(data, 2),
|
||||
'paymentMethod': _getVal(data, 13) == 'true' ? 'visa' : 'cash',
|
||||
'isHaveSteps': _getVal(data, 20),
|
||||
'step0': _getVal(data, 21),
|
||||
'step1': _getVal(data, 22),
|
||||
'step2': _getVal(data, 23),
|
||||
'step3': _getVal(data, 24),
|
||||
'step4': _getVal(data, 25),
|
||||
'passengerWalletBurc': _getVal(data, 26),
|
||||
'timeOfOrder': DateTime.now().toString(),
|
||||
'totalPassenger': _getVal(data, 2),
|
||||
'carType': _getVal(data, 31),
|
||||
'kazan': _getVal(data, 32),
|
||||
'startNameLocation': _getVal(data, 29),
|
||||
'endNameLocation': _getVal(data, 30),
|
||||
};
|
||||
}
|
||||
|
||||
String _getVal(List<dynamic> data, int index) {
|
||||
if (data.length > index && data[index] != null) {
|
||||
return data[index].toString();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
void onDidReceiveNotificationResponse(NotificationResponse response) {
|
||||
handleNotificationResponse(response);
|
||||
}
|
||||
|
||||
void onDidReceiveBackgroundNotificationResponse(
|
||||
NotificationResponse response) {
|
||||
handleNotificationResponse(response);
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// 7. الدوال القديمة (Old Scheduled Notifications) - لم يتم تغييرها
|
||||
// ==============================================================================
|
||||
|
||||
void showNotification(
|
||||
String title, String message, String tone, String payLoad) async {
|
||||
// هذه الدالة القديمة للإشعارات البسيطة (ليس الطلبات)
|
||||
BigTextStyleInformation bigTextStyleInformation = BigTextStyleInformation(
|
||||
message,
|
||||
contentTitle: title.tr,
|
||||
htmlFormatContent: true,
|
||||
htmlFormatContentTitle: true,
|
||||
);
|
||||
AndroidNotificationDetails android = AndroidNotificationDetails(
|
||||
'high_importance_channel',
|
||||
'High Importance Notifications',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
sound: RawResourceAndroidNotificationSound(tone.split('.').first),
|
||||
);
|
||||
|
||||
DarwinNotificationDetails ios = const DarwinNotificationDetails(
|
||||
sound: 'default',
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
NotificationDetails details =
|
||||
NotificationDetails(android: android, iOS: ios);
|
||||
|
||||
await _flutterLocalNotificationsPlugin.show(
|
||||
id: 0,
|
||||
title: title,
|
||||
body: message,
|
||||
notificationDetails: details,
|
||||
payload: jsonEncode({'title': title, 'data': payLoad}));
|
||||
}
|
||||
|
||||
void scheduleNotificationsForSevenDays(
|
||||
String title, String message, String tone) async {
|
||||
final AndroidNotificationDetails android = AndroidNotificationDetails(
|
||||
'high_importance_channel',
|
||||
'High Importance Notifications',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
sound: RawResourceAndroidNotificationSound(tone.split('.').first),
|
||||
);
|
||||
|
||||
const DarwinNotificationDetails ios = DarwinNotificationDetails(
|
||||
sound: 'default',
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
final NotificationDetails details =
|
||||
NotificationDetails(android: android, iOS: ios);
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
if (await Permission.scheduleExactAlarm.isDenied) {
|
||||
await Permission.scheduleExactAlarm.request();
|
||||
}
|
||||
}
|
||||
|
||||
for (int day = 0; day < 7; day++) {
|
||||
final notificationTimes = [
|
||||
{'hour': 8, 'minute': 0, 'id': day * 1000 + 1},
|
||||
{'hour': 15, 'minute': 0, 'id': day * 1000 + 2},
|
||||
{'hour': 20, 'minute': 0, 'id': day * 1000 + 3},
|
||||
];
|
||||
|
||||
for (var time in notificationTimes) {
|
||||
final notificationId = time['id'] as int;
|
||||
bool isScheduled = box.read('notification_$notificationId') ?? false;
|
||||
|
||||
if (!isScheduled) {
|
||||
await _scheduleNotificationForTime(
|
||||
day,
|
||||
time['hour'] as int,
|
||||
time['minute'] as int,
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
notificationId,
|
||||
);
|
||||
box.write('notification_$notificationId', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================================================
|
||||
// دالة حذف إشعار الطلب (تستدعى عند أخذ الطلب من سائق آخر)
|
||||
// ==============================================================================
|
||||
Future<void> cancelOrderNotification() async {
|
||||
// 1001 هو نفس الآيدي الذي استخدمناه عند عرض الإشعار
|
||||
await _flutterLocalNotificationsPlugin.cancel(id: 1001);
|
||||
print("🗑️ Order Notification Cancelled (Taken by another driver)");
|
||||
}
|
||||
|
||||
Future<void> _scheduleNotificationForTime(
|
||||
int dayOffset,
|
||||
int hour,
|
||||
int minute,
|
||||
String title,
|
||||
String message,
|
||||
NotificationDetails details,
|
||||
int notificationId,
|
||||
) async {
|
||||
tz.initializeTimeZones();
|
||||
var cairoLocation =
|
||||
tz.getLocation('Africa/Cairo'); // تأكد من المنطقة الزمنية
|
||||
|
||||
final now = tz.TZDateTime.now(cairoLocation);
|
||||
tz.TZDateTime scheduledDate = tz.TZDateTime(
|
||||
cairoLocation,
|
||||
now.year,
|
||||
now.month,
|
||||
now.day + dayOffset,
|
||||
hour,
|
||||
minute,
|
||||
);
|
||||
|
||||
if (scheduledDate.isBefore(now)) {
|
||||
scheduledDate = scheduledDate.add(const Duration(days: 1));
|
||||
}
|
||||
|
||||
await _flutterLocalNotificationsPlugin.zonedSchedule(
|
||||
id: notificationId,
|
||||
title: title,
|
||||
body: message,
|
||||
scheduledDate: scheduledDate,
|
||||
notificationDetails: details,
|
||||
androidScheduleMode: AndroidScheduleMode.exact, // أو exactAllowWhileIdle
|
||||
matchDateTimeComponents: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:get/get.dart'; // للترجمة .tr
|
||||
|
||||
class NotificationService {
|
||||
static const String _serverUrl =
|
||||
'https://api.intaleq.xyz/intaleq/ride/firebase/send_fcm.php';
|
||||
|
||||
static Future<void> sendNotification({
|
||||
required String target,
|
||||
required String title,
|
||||
required String body,
|
||||
required String category, // إلزامي للتصنيف
|
||||
String? tone,
|
||||
List<String>? driverList,
|
||||
bool isTopic = false,
|
||||
}) async {
|
||||
try {
|
||||
// 1. تجهيز البيانات المخصصة (Data Payload)
|
||||
Map<String, dynamic> customData = {};
|
||||
|
||||
customData['category'] = category;
|
||||
|
||||
// إذا كان هناك قائمة سائقين/ركاب، نضعها هنا
|
||||
if (driverList != null && driverList.isNotEmpty) {
|
||||
// نرسلها كـ JSON String لأن FCM v1 يدعم String Values فقط في الـ data
|
||||
customData['driverList'] = jsonEncode(driverList);
|
||||
}
|
||||
|
||||
// 2. تجهيز الطلب الرئيسي للسيرفر
|
||||
final Map<String, dynamic> requestPayload = {
|
||||
'target': target,
|
||||
'title': title,
|
||||
'body': body,
|
||||
'isTopic': isTopic,
|
||||
'data':
|
||||
customData, // 🔥🔥 التغيير الجوهري: وضعنا البيانات داخل "data" 🔥🔥
|
||||
};
|
||||
|
||||
if (tone != null) {
|
||||
requestPayload['tone'] = tone;
|
||||
}
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse(_serverUrl),
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
body: jsonEncode(requestPayload),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
print('✅ Notification sent successfully.');
|
||||
// print('Response: ${response.body}');
|
||||
} else {
|
||||
print('❌ Failed to send notification. Code: ${response.statusCode}');
|
||||
print('Error Body: ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Error sending notification: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
35
siro_driver/lib/controller/firebase/order_lay.dart
Executable file
35
siro_driver/lib/controller/firebase/order_lay.dart
Executable file
@@ -0,0 +1,35 @@
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OverlayContent1 extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
color: Colors.white,
|
||||
child: MyElevatedButton(
|
||||
title: 'go to order',
|
||||
onPressed: () async {
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.addFeedBack,
|
||||
payload: {
|
||||
"passengerId": 'dddddd',
|
||||
"feedBack": "eeeee",
|
||||
},
|
||||
);
|
||||
print(res);
|
||||
if (res != 'failure') {
|
||||
Navigator.push(
|
||||
context, MaterialPageRoute(builder: (cont) => HomeCaptain()));
|
||||
// Get.to(OrderRequestPage());
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
32
siro_driver/lib/controller/functions/add_error.dart
Executable file
32
siro_driver/lib/controller/functions/add_error.dart
Executable file
@@ -0,0 +1,32 @@
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../main.dart';
|
||||
import 'crud.dart';
|
||||
|
||||
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);
|
||||
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../constant/info.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../print.dart';
|
||||
import 'crud.dart';
|
||||
|
||||
class AppUpdateController extends GetxController {
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// الفحص التلقائي عند التشغيل لتحديثات المتجر
|
||||
checkSmartUpdate();
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// الدالة الذكية المدمجة (الآن تفحص المتجر فقط لأن Shorebird يعمل تلقائياً بالخلفية)
|
||||
// ======================================================================
|
||||
Future<void> checkSmartUpdate() async {
|
||||
Log.print("🔄 بدء فحص تحديثات المتجر...");
|
||||
|
||||
// 1. فحص تحديث المتجر (Native Update)
|
||||
await _checkStoreUpdate();
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 1. تحديث المتجر الأساسي
|
||||
// ======================================================================
|
||||
Future<bool> _checkStoreUpdate() async {
|
||||
try {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final currentBuildNumber = packageInfo.buildNumber;
|
||||
|
||||
// استخدام نفس الـ Endpoint والمعايير الموجودة في التطبيق
|
||||
var response = await CRUD().get(link: AppLink.packageInfo, payload: {
|
||||
"platform": Platform.isAndroid ? 'android' : 'ios',
|
||||
"appName": AppInformation.appVersion,
|
||||
});
|
||||
|
||||
if (response != 'failure') {
|
||||
var decoded = jsonDecode(response);
|
||||
if (decoded['status'] == 'success' && decoded['message'] != null && decoded['message'].isNotEmpty) {
|
||||
String latestBuildNumber = decoded['message'][0]['version'].toString();
|
||||
|
||||
// مقارنة الـ Build Number
|
||||
if (latestBuildNumber != currentBuildNumber) {
|
||||
_showStoreUpdateDialog();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("❌ Store update check error: $e");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// دوال مساعدة
|
||||
// ======================================================================
|
||||
|
||||
void _showStoreUpdateDialog() {
|
||||
final String storeUrl = Platform.isAndroid
|
||||
? 'https://play.google.com/store/apps/details?id=com.intaleq_driver'
|
||||
: 'https://apps.apple.com/jo/app/intaleq-driver/id6482995159';
|
||||
|
||||
Get.defaultDialog(
|
||||
title: "تحديث جديد متوفر".tr,
|
||||
middleText: "يوجد إصدار جديد من التطبيق في المتجر، يرجى التحديث للحصول على الميزات الجديدة.".tr,
|
||||
barrierDismissible: false,
|
||||
onWillPop: () async => false,
|
||||
confirm: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))
|
||||
),
|
||||
onPressed: () async {
|
||||
if (await canLaunchUrl(Uri.parse(storeUrl))) {
|
||||
await launchUrl(Uri.parse(storeUrl), mode: LaunchMode.externalApplication);
|
||||
}
|
||||
},
|
||||
child: Text("تحديث الآن".tr, style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
44
siro_driver/lib/controller/functions/audio_controller.dart
Executable file
44
siro_driver/lib/controller/functions/audio_controller.dart
Executable file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class AudioController extends GetxController {
|
||||
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||
|
||||
Future<void> playAudio() async {
|
||||
// Check if the platform is Android
|
||||
if (Theme.of(Get.context!).platform == TargetPlatform.android) {
|
||||
try {
|
||||
// Load the audio file from the raw resources
|
||||
await _audioPlayer.setAsset(
|
||||
'assets/order1.wav'); // Adjust the path based on your project structure
|
||||
_audioPlayer.play();
|
||||
} catch (e) {
|
||||
// Handle errors, such as file not found
|
||||
print('Error playing audio: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> playAudio1(String path) async {
|
||||
// Check if the platform is Android
|
||||
// if (Theme.of(Get.context!).platform == TargetPlatform.android) {
|
||||
try {
|
||||
// Load the audio file from the raw resources
|
||||
await _audioPlayer
|
||||
.setAsset(path); // Adjust the path based on your project structure
|
||||
_audioPlayer.play();
|
||||
} catch (e) {
|
||||
// Handle errors, such as file not found
|
||||
print('Error playing audio: $e');
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// Release resources when done
|
||||
_audioPlayer.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'dart:io';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:record/record.dart';
|
||||
|
||||
class AudioRecorderController extends GetxController {
|
||||
AudioPlayer audioPlayer = AudioPlayer();
|
||||
AudioRecorder recorder = AudioRecorder();
|
||||
|
||||
bool isRecording = false;
|
||||
bool isPlaying = false;
|
||||
bool isPaused = false;
|
||||
String filePath = '';
|
||||
String? selectedFilePath;
|
||||
double currentPosition = 0;
|
||||
double totalDuration = 0;
|
||||
|
||||
// Start recording
|
||||
Future<void> startRecording({String? rideId}) async {
|
||||
final bool isPermissionGranted = await recorder.hasPermission();
|
||||
if (!isPermissionGranted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final String dateStr =
|
||||
'${DateTime.now().year}-${DateTime.now().month.toString().padLeft(2, '0')}-${DateTime.now().day.toString().padLeft(2, '0')}';
|
||||
// Generate a unique file name
|
||||
String fileName = (rideId != null && rideId.isNotEmpty && rideId != 'yet' && rideId != 'null')
|
||||
? '${dateStr}_$rideId.m4a'
|
||||
: '$dateStr.m4a';
|
||||
filePath = '${directory.path}/$fileName';
|
||||
|
||||
const config = RecordConfig(
|
||||
encoder: AudioEncoder.aacLc,
|
||||
sampleRate: 44100,
|
||||
bitRate: 128000,
|
||||
);
|
||||
|
||||
await recorder.start(config, path: filePath);
|
||||
|
||||
isRecording = true;
|
||||
update();
|
||||
}
|
||||
|
||||
// Stop recording
|
||||
Future<void> stopRecording() async {
|
||||
await recorder.stop();
|
||||
isRecording = false;
|
||||
isPaused = false;
|
||||
update();
|
||||
}
|
||||
|
||||
// Get a list of recorded files
|
||||
Future<List<String>> getRecordedFiles() async {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final files = await directory.list().toList();
|
||||
return files
|
||||
.map((file) => file.path)
|
||||
.where((path) => path.endsWith('.m4a'))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Delete a specific recorded file
|
||||
Future<void> deleteRecordedFile(String filePath) async {
|
||||
final file = File(filePath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
audioPlayer.dispose();
|
||||
recorder.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
225
siro_driver/lib/controller/functions/background_service.dart
Normal file
225
siro_driver/lib/controller/functions/background_service.dart
Normal file
@@ -0,0 +1,225 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart' as IO;
|
||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay;
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:geolocator/geolocator.dart' as geo;
|
||||
import '../../constant/box_name.dart';
|
||||
import '../firebase/local_notification.dart';
|
||||
|
||||
const String notificationChannelId = 'driver_service_channel';
|
||||
const int notificationId = 888;
|
||||
const String notificationIcon = '@mipmap/launcher_icon';
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<bool> onStart(ServiceInstance service) async {
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
|
||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
await GetStorage.init();
|
||||
final box = GetStorage();
|
||||
|
||||
IO.Socket? socket;
|
||||
String driverId = box.read(BoxName.driverID) ?? '';
|
||||
String token = box.read(BoxName.tokenDriver) ?? '';
|
||||
|
||||
if (driverId.isNotEmpty) {
|
||||
socket = IO.io(
|
||||
'https://location.intaleq.xyz',
|
||||
IO.OptionBuilder()
|
||||
.setTransports(['websocket'])
|
||||
.disableAutoConnect()
|
||||
.setQuery({
|
||||
'driver_id': driverId,
|
||||
'token': token,
|
||||
'EIO': '3', // توافقية مع Workerman
|
||||
})
|
||||
.setReconnectionAttempts(double.infinity)
|
||||
.build());
|
||||
|
||||
socket.connect();
|
||||
|
||||
socket.onConnect((_) {
|
||||
print("✅ Background Service: Socket Connected! ID: ${socket?.id}");
|
||||
if (service is AndroidServiceInstance) {
|
||||
flutterLocalNotificationsPlugin.show(
|
||||
id: notificationId,
|
||||
title: 'أنت متصل الآن',
|
||||
body: 'بانتظار الطلبات...',
|
||||
notificationDetails: const NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
notificationChannelId,
|
||||
'خدمة السائق',
|
||||
icon: notificationIcon,
|
||||
ongoing: true,
|
||||
importance: Importance.low,
|
||||
priority: Priority.low,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('new_ride_request', (data) async {
|
||||
print("🔔 Background Service: Received new_ride_request");
|
||||
|
||||
// 🔥 قراءة حالة التطبيق مباشرة قبل العرض
|
||||
await GetStorage.init(); // تأكد من تحديث البيانات
|
||||
final box = GetStorage();
|
||||
bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false;
|
||||
|
||||
// 🔥 Check إضافي: هل الـ Overlay مفتوح بالفعل؟ (للأندرويد فقط)
|
||||
bool overlayActive = false;
|
||||
if (Platform.isAndroid) {
|
||||
overlayActive = await Overlay.FlutterOverlayWindow.isActive();
|
||||
}
|
||||
|
||||
if (isAppInForeground || overlayActive) {
|
||||
print("🛑 App is FOREGROUND or Overlay already shown. Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
// عرض الـ Overlay (للأندرويد فقط)
|
||||
if (Platform.isAndroid) {
|
||||
print("🚀 App is BACKGROUND. Showing Overlay...");
|
||||
try {
|
||||
await Overlay.FlutterOverlayWindow.showOverlay(
|
||||
enableDrag: true,
|
||||
overlayTitle: "طلب جديد",
|
||||
overlayContent: "لديك طلب جديد وصل للتو!",
|
||||
flag: OverlayFlag.focusPointer,
|
||||
positionGravity: PositionGravity.auto,
|
||||
height: WindowSize.matchParent,
|
||||
width: WindowSize.matchParent,
|
||||
startPosition: const OverlayPosition(0, -30),
|
||||
);
|
||||
await Overlay.FlutterOverlayWindow.shareData(data);
|
||||
} catch (e) {
|
||||
print("Overlay Error: $e");
|
||||
}
|
||||
} else if (Platform.isIOS) {
|
||||
// على iOS، نظهر إشعاراً عادياً لأن الـ Overlay غير موجود
|
||||
flutterLocalNotificationsPlugin.show(
|
||||
id: 1002,
|
||||
title: "طلب رحلة جديد 🚖",
|
||||
body: "لديك طلب رحلة جديد، افتح التطبيق للموافقة عليه",
|
||||
notificationDetails: const NotificationDetails(
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
),
|
||||
),
|
||||
payload: jsonEncode(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
service.on('stopService').listen((event) {
|
||||
socket?.clearListeners();
|
||||
socket?.dispose();
|
||||
service.stopSelf();
|
||||
});
|
||||
|
||||
// 🔥 Location management in background isolate (Using Geolocator)
|
||||
geo.Position? latestPos;
|
||||
|
||||
// Listen to location changes continuously in the background
|
||||
geo.Geolocator.getPositionStream(
|
||||
locationSettings: geo.AndroidSettings(
|
||||
accuracy: geo.LocationAccuracy.high,
|
||||
distanceFilter: 10,
|
||||
intervalDuration: const Duration(seconds: 10),
|
||||
),
|
||||
).listen((pos) {
|
||||
latestPos = pos;
|
||||
});
|
||||
|
||||
// 🔥 MERCY HEARTBEAT: Send location every 2 minutes to keep driver active in 'raids'
|
||||
Timer.periodic(const Duration(minutes: 2), (timer) async {
|
||||
if (socket != null && socket.connected && latestPos != null) {
|
||||
try {
|
||||
socket.emit('update_location', {
|
||||
'driver_id': driverId,
|
||||
'lat': latestPos!.latitude,
|
||||
'lng': latestPos!.longitude,
|
||||
'heading': latestPos!.heading,
|
||||
'speed': latestPos!.speed * 3.6,
|
||||
'status': box.read(BoxName.statusDriverLocation) ?? 'on',
|
||||
'source': 'background_heartbeat'
|
||||
});
|
||||
print(
|
||||
"💓 Background Mercy Heartbeat Sent: ${latestPos!.latitude}, ${latestPos!.longitude}");
|
||||
} catch (e) {
|
||||
print("❌ Background Heartbeat Error: $e");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||
if (service is AndroidServiceInstance) {
|
||||
if (await service.isForegroundService()) {
|
||||
flutterLocalNotificationsPlugin.show(
|
||||
id: notificationId,
|
||||
title: 'خدمة السائق نشطة',
|
||||
body: 'بانتظار الطلبات...',
|
||||
notificationDetails: const NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
notificationChannelId,
|
||||
'خدمة السائق',
|
||||
icon: notificationIcon,
|
||||
ongoing: true,
|
||||
importance: Importance.low,
|
||||
priority: Priority.low,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
class BackgroundServiceHelper {
|
||||
static Future<void> initialize() async {
|
||||
final service = FlutterBackgroundService();
|
||||
|
||||
await service.configure(
|
||||
androidConfiguration: AndroidConfiguration(
|
||||
onStart: onStart,
|
||||
autoStart: false,
|
||||
isForegroundMode: true,
|
||||
notificationChannelId: notificationChannelId,
|
||||
initialNotificationTitle: 'تطبيق السائق',
|
||||
initialNotificationContent: 'تجهيز الخدمة...',
|
||||
foregroundServiceNotificationId: notificationId,
|
||||
),
|
||||
iosConfiguration: IosConfiguration(
|
||||
autoStart: false,
|
||||
onForeground: onStart,
|
||||
onBackground: onStart,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> startService() async {
|
||||
final service = FlutterBackgroundService();
|
||||
if (!await service.isRunning()) {
|
||||
await service.startService();
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> stopService() async {
|
||||
final service = FlutterBackgroundService();
|
||||
service.invoke("stopService");
|
||||
}
|
||||
}
|
||||
39
siro_driver/lib/controller/functions/battery_status.dart
Normal file
39
siro_driver/lib/controller/functions/battery_status.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:battery_plus/battery_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class BatteryNotifier {
|
||||
static final Battery _battery = Battery();
|
||||
static int? _lastNotifiedLevel;
|
||||
|
||||
static Future<void> checkBatteryAndNotify() async {
|
||||
try {
|
||||
final int batteryLevel = await _battery.batteryLevel;
|
||||
|
||||
// ✅ لا تكرر الإشعار إذا الفرق قليل
|
||||
if (_lastNotifiedLevel != null &&
|
||||
(batteryLevel >= _lastNotifiedLevel! - 2)) return;
|
||||
|
||||
if (batteryLevel <= 30) {
|
||||
Color backgroundColor = Colors.yellow;
|
||||
if (batteryLevel <= 20) {
|
||||
backgroundColor = Colors.red;
|
||||
}
|
||||
|
||||
Get.snackbar(
|
||||
"⚠️ تنبيه البطارية", // العنوان
|
||||
"مستوى البطارية: $batteryLevel٪", // النص
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: backgroundColor,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 10), // مدة الظهور
|
||||
margin: const EdgeInsets.all(10),
|
||||
);
|
||||
|
||||
_lastNotifiedLevel = batteryLevel;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Battery check error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
129
siro_driver/lib/controller/functions/call_controller.dart
Executable file
129
siro_driver/lib/controller/functions/call_controller.dart
Executable file
@@ -0,0 +1,129 @@
|
||||
// import 'package:SEFER/constant/api_key.dart';
|
||||
// import 'package:SEFER/controller/functions/crud.dart';
|
||||
// // import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
// import '../../constant/box_name.dart';
|
||||
// import '../firebase/firbase_messge.dart';
|
||||
// import '../home/captin/map_driver_controller.dart';
|
||||
// import '../../main.dart';
|
||||
|
||||
// class CallController extends GetxController {
|
||||
// String channelName = ''; // Get.find<MapDriverController>().rideId;
|
||||
// String token = '';
|
||||
// // int uid = int.parse(box.read(BoxName.phoneDriver)); // uid of the local user
|
||||
// int uid = 0;
|
||||
// int? remoteUid; // uid of the remote user
|
||||
// bool _isJoined = false; // Indicates if the local user has joined the channel
|
||||
// String status = '';
|
||||
// // late RtcEngine agoraEngine; // Agora engine instance
|
||||
|
||||
// @override
|
||||
// void onInit() {
|
||||
// super.onInit();
|
||||
|
||||
// channelName = Get.find<MapDriverController>().rideId; // 'sefer300'; //
|
||||
// remoteUid = int.parse(Get.find<MapDriverController>().passengerPhone);
|
||||
// uid = int.parse(box.read(BoxName.phoneDriver));
|
||||
|
||||
// initAgoraFull();
|
||||
// }
|
||||
|
||||
// initAgoraFull() async {
|
||||
// await fetchToken();
|
||||
// // Set up an instance of Agora engine
|
||||
// setupVoiceSDKEngine();
|
||||
// // join();
|
||||
// FirebaseMessagesController().sendNotificationToPassengerTokenCALL(
|
||||
// 'Call Income',
|
||||
// '${'You have call from driver'.tr} ${box.read(BoxName.nameDriver)}',
|
||||
// Get.find<MapDriverController>().tokenPassenger,
|
||||
// [
|
||||
// token,
|
||||
// channelName,
|
||||
// uid.toString(),
|
||||
// remoteUid.toString(),
|
||||
// ],
|
||||
// );
|
||||
// join();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// void onClose() {
|
||||
// // agoraEngine.leaveChannel();
|
||||
// super.onClose();
|
||||
// }
|
||||
|
||||
// // Future<void> setupVoiceSDKEngine() async {
|
||||
// // // retrieve or request microphone permission
|
||||
// // await [Permission.microphone].request();
|
||||
|
||||
// // //create an instance of the Agora engine
|
||||
// // agoraEngine = createAgoraRtcEngine();
|
||||
// // await agoraEngine.initialize(RtcEngineContext(appId: AK.agoraAppId));
|
||||
// // // Register the event handler
|
||||
// // agoraEngine.registerEventHandler(
|
||||
// // RtcEngineEventHandler(
|
||||
// // onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
|
||||
// // // Get.snackbar(
|
||||
// // // "Local user uid:${connection.localUid} joined the channel", '');
|
||||
// // status = 'joined'.tr;
|
||||
// // _isJoined = true;
|
||||
// // update();
|
||||
// // },
|
||||
// // onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
|
||||
// // // Get.snackbar("Remote user uid:$remoteUid joined the channel", '');
|
||||
// // status = '${Get.find<MapDriverController>().passengerName} '
|
||||
// // 'joined'
|
||||
// // .tr;
|
||||
// // remoteUid = remoteUid;
|
||||
// // update();
|
||||
// // },
|
||||
// // onUserOffline: (RtcConnection connection, int? remoteUid,
|
||||
// // UserOfflineReasonType reason) {
|
||||
// // // Get.snackbar("Remote user uid:$remoteUid left the channel", '');
|
||||
// // status = 'Call Left'.tr;
|
||||
// // remoteUid = null;
|
||||
// // update();
|
||||
// // },
|
||||
// // ),
|
||||
// // );
|
||||
// // }
|
||||
|
||||
// // void join() async {
|
||||
// // // Set channel options including the client role and channel profile
|
||||
// // ChannelMediaOptions options = const ChannelMediaOptions(
|
||||
// // clientRoleType: ClientRoleType.clientRoleBroadcaster,
|
||||
// // channelProfile: ChannelProfileType.channelProfileCommunication,
|
||||
// // );
|
||||
|
||||
// // await agoraEngine.joinChannel(
|
||||
// // token: token,
|
||||
// // channelId: channelName,
|
||||
// // options: options,
|
||||
// // uid: uid,
|
||||
// // );
|
||||
// // }
|
||||
|
||||
// // void leave() {
|
||||
// // _isJoined = false;
|
||||
// // remoteUid = null;
|
||||
// // update();
|
||||
// // agoraEngine.leaveChannel();
|
||||
// // }
|
||||
|
||||
// // // Clean up the resources when you leave
|
||||
// // @override
|
||||
// // void dispose() async {
|
||||
// // await agoraEngine.leaveChannel();
|
||||
// // super.dispose();
|
||||
// // }
|
||||
|
||||
// fetchToken() async {
|
||||
// var res = await CRUD()
|
||||
// .getAgoraToken(channelName: channelName, uid: uid.toString());
|
||||
// token = res;
|
||||
// update();
|
||||
// }
|
||||
// }
|
||||
238
siro_driver/lib/controller/functions/camer_controller.dart
Executable file
238
siro_driver/lib/controller/functions/camer_controller.dart
Executable file
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:get/get.dart';
|
||||
// import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../../main.dart';
|
||||
|
||||
class CameraClassController extends GetxController {
|
||||
late CameraController cameraController;
|
||||
late List<CameraDescription> cameras;
|
||||
bool isCameraInitialized = false;
|
||||
// final TextRecognizer _textRecognizer = TextRecognizer();
|
||||
String? scannedText;
|
||||
bool isloading = false;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
initializeCamera();
|
||||
}
|
||||
|
||||
Future<void> initializeCamera() async {
|
||||
try {
|
||||
cameras = await availableCameras();
|
||||
//update();
|
||||
cameraController = CameraController(
|
||||
cameras[0],
|
||||
ResolutionPreset.medium,
|
||||
enableAudio: false,
|
||||
);
|
||||
await cameraController.initialize();
|
||||
isCameraInitialized = true;
|
||||
update();
|
||||
} catch (e) {
|
||||
if (e is CameraException) {
|
||||
switch (e.code) {
|
||||
case 'CameraAccessDenied':
|
||||
Get.defaultDialog(
|
||||
title: 'Camera Access Denied.'.tr,
|
||||
middleText: '',
|
||||
confirm:
|
||||
MyElevatedButton(title: 'Open Settings'.tr, onPressed: () {}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Handle other errors here.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var imgUrl = '';
|
||||
Future extractCardId() async {
|
||||
// Construct the path for the image file
|
||||
final directory = await path_provider.getTemporaryDirectory();
|
||||
final imagePath =
|
||||
path.join(directory.path, '${box.read(BoxName.driverID)}.png');
|
||||
|
||||
// Capture the image and save it to the specified path
|
||||
final XFile capturedImage = await cameraController.takePicture();
|
||||
|
||||
// Move the captured image to the desired path
|
||||
await capturedImage.saveTo(imagePath);
|
||||
await uploadImage(File(capturedImage.path));
|
||||
|
||||
extractByAPI('${AppLink.server}/card_image/' + box.read(BoxName.driverID));
|
||||
}
|
||||
|
||||
Future extractByAPI(String imgUrl) async {
|
||||
var headers = {'apikey': 'K89368168788957'};
|
||||
var request = http.MultipartRequest(
|
||||
'POST', Uri.parse('https://api.ocr.space/parse/image'));
|
||||
request.fields.addAll({
|
||||
'language': 'ara',
|
||||
'isOverlayRequired': 'false',
|
||||
'url': imgUrl,
|
||||
'iscreatesearchablepdf': 'false',
|
||||
'issearchablepdfhidetextlayer': 'false'
|
||||
});
|
||||
|
||||
request.headers.addAll(headers);
|
||||
|
||||
http.StreamedResponse response = await request.send();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
} else {}
|
||||
}
|
||||
|
||||
Future<String> uploadImage(File imageFile) async {
|
||||
String? basicAuthCredentials =
|
||||
await storage.read(key: BoxName.basicAuthCredentials);
|
||||
var request = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse(AppLink.uploadImage),
|
||||
);
|
||||
|
||||
// Attach the image file to the request
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath('image', imageFile.path),
|
||||
); // Add the headers to the request
|
||||
request.headers.addAll({
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
'Authorization':
|
||||
'Basic ${base64Encode(utf8.encode(basicAuthCredentials.toString()))}',
|
||||
});
|
||||
|
||||
// Add the driverID to the request
|
||||
request.fields['driverID'] = box.read(BoxName.driverID);
|
||||
// Send the request
|
||||
var response = await request.send();
|
||||
|
||||
// Read the response
|
||||
var responseData = await response.stream.toBytes();
|
||||
var responseString = String.fromCharCodes(responseData);
|
||||
scannedText = responseString;
|
||||
update();
|
||||
// Return the link received from the server
|
||||
return responseString;
|
||||
}
|
||||
|
||||
// Future<void> takePictureAndMLGoogleScan() async {
|
||||
// try {
|
||||
// // Construct the path for the image file
|
||||
// final directory = await path_provider.getTemporaryDirectory();
|
||||
// final imagePath =
|
||||
// path.join(directory.path, '${box.read(BoxName.driverID)}.png');
|
||||
|
||||
// // Capture the image and save it to the specified path
|
||||
// final XFile capturedImage = await cameraController.takePicture();
|
||||
|
||||
// // Move the captured image to the desired path
|
||||
// await capturedImage.saveTo(imagePath);
|
||||
|
||||
// // Recognize the text in the image
|
||||
// final InputImage inputImage =
|
||||
// InputImage.fromFile(File(capturedImage.path));
|
||||
// final RecognizedText recognizedText =
|
||||
// await _textRecognizer.processImage(inputImage);
|
||||
// scannedText = recognizedText.text;
|
||||
|
||||
// // Extract the scanned text line by line
|
||||
// final List<Map<String, dynamic>> lines = [];
|
||||
// for (var i = 0; i < recognizedText.blocks.length; i++) {
|
||||
// lines.add({
|
||||
// 'line_number': i,
|
||||
// 'text': recognizedText.blocks[i].text,
|
||||
// });
|
||||
// }
|
||||
|
||||
// // Convert the list of lines to a JSON string
|
||||
// final String jsonOutput = jsonEncode(lines);
|
||||
|
||||
// update();
|
||||
|
||||
// // Print the JSON output
|
||||
|
||||
// // Get.back();
|
||||
// } catch (e) {}
|
||||
// }
|
||||
|
||||
String getTextAsJSON(String text) {
|
||||
final lines = text.split('\n');
|
||||
final jsonList = lines.map((line) {
|
||||
return {
|
||||
'line_text': line,
|
||||
'num_words': line.trim().split(' ').length,
|
||||
};
|
||||
}).toList();
|
||||
|
||||
final json = {
|
||||
'lines': jsonList,
|
||||
'num_lines': lines.length,
|
||||
};
|
||||
|
||||
return jsonEncode(json);
|
||||
}
|
||||
|
||||
List<String> getTextBlocks(String text) {
|
||||
return text.split('\n');
|
||||
}
|
||||
|
||||
// Future<void> takePictureAndTesseractScan() async {
|
||||
// try {
|
||||
// // Construct the path for the image file
|
||||
// final directory = await path_provider.getTemporaryDirectory();
|
||||
// final imagePath =
|
||||
// path.join(directory.path, '${box.read(BoxName.driverID)}.png');
|
||||
|
||||
// // Capture the image and save it to the specified path
|
||||
// final XFile capturedImage = await cameraController.takePicture();
|
||||
|
||||
// // Move the captured image to the desired path
|
||||
// await capturedImage.saveTo(imagePath);
|
||||
|
||||
// // Recognize the text in the image
|
||||
// final languages = [
|
||||
// 'eng',
|
||||
// 'ara'
|
||||
// ]; // Specify the languages you want to use for text extraction
|
||||
|
||||
// final text = await FlutterTesseractOcr.extractText(imagePath,
|
||||
// language: languages.join('+'), // Combine multiple languages with '+'
|
||||
// args: {
|
||||
// "psm": "4",
|
||||
// "preserve_interword_spaces": "1",
|
||||
// // "rectangle": const Rect.fromLTWH(100, 100, 200, 200),
|
||||
// } // Additional options if needed
|
||||
// );
|
||||
// isloading = false;
|
||||
// final jsonText = getTextAsJSON(text);
|
||||
// final textBlocks = getTextBlocks(text);
|
||||
// update();
|
||||
// scannedText =
|
||||
// textBlocks.toString(); // Convert the extracted text to JSON.
|
||||
|
||||
// // Print the JSON to the console.
|
||||
// update();
|
||||
// } catch (e) {
|
||||
// scannedText = '';
|
||||
// }
|
||||
// }
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
cameraController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
*/
|
||||
736
siro_driver/lib/controller/functions/crud.dart
Executable file
736
siro_driver/lib/controller/functions/crud.dart
Executable file
@@ -0,0 +1,736 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:siro_driver/controller/functions/encrypt_decrypt.dart';
|
||||
import 'package:siro_driver/controller/functions/network/net_guard.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:siro_driver/env/env.dart';
|
||||
import 'package:siro_driver/print.dart';
|
||||
|
||||
import '../../constant/api_key.dart';
|
||||
import '../../views/widgets/error_snakbar.dart';
|
||||
import 'gemeni.dart';
|
||||
import 'upload_image.dart';
|
||||
|
||||
class CRUD {
|
||||
final NetGuard _netGuard = NetGuard();
|
||||
|
||||
static bool _isRefreshingJWT = false;
|
||||
static String _lastErrorSignature = '';
|
||||
static DateTime _lastErrorTimestamp = DateTime(2000);
|
||||
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
|
||||
|
||||
// ── فحص صلاحية JWT بدون مكتبات خارجية ──────────────────────
|
||||
static bool _isJwtValid(String? token) {
|
||||
if (token == null || token.isEmpty) return false;
|
||||
try {
|
||||
final parts = token.split('.');
|
||||
if (parts.length != 3) return false;
|
||||
// فك تشفير الـ payload (الجزء الثاني)
|
||||
String payload = parts[1];
|
||||
// إضافة padding للـ base64
|
||||
switch (payload.length % 4) {
|
||||
case 2: payload += '=='; break;
|
||||
case 3: payload += '='; break;
|
||||
}
|
||||
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
|
||||
final exp = decoded['exp'];
|
||||
if (exp == null) return false;
|
||||
// نعتبر التوكن منتهي قبل 30 ثانية من انتهاء الصلاحية (buffer)
|
||||
return DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> addError(
|
||||
String error, String details, String where) async {
|
||||
try {
|
||||
final currentErrorSignature = '$where-$error';
|
||||
final now = DateTime.now();
|
||||
|
||||
if (currentErrorSignature == _lastErrorSignature &&
|
||||
now.difference(_lastErrorTimestamp) < _errorLogDebounceDuration) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastErrorSignature = currentErrorSignature;
|
||||
_lastErrorTimestamp = now;
|
||||
|
||||
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);
|
||||
|
||||
CRUD().post(
|
||||
link: AppLink.addError,
|
||||
payload: {
|
||||
'error': error.toString(),
|
||||
'userId': userId.toString(),
|
||||
'userType': userType,
|
||||
'phone': phone.toString(),
|
||||
'device': where,
|
||||
'details': details,
|
||||
},
|
||||
);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// دالة مساعدة: يجيب البصمة المشفرة من GetStorage
|
||||
// نفس القيمة المرسلة عند login وعُملها hash في JWT
|
||||
// السيرفر يتحقق: sha256(X-Device-FP + FP_PEPPER) == JWT.fingerPrint
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
String _getFpHeader() {
|
||||
return box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// _makeRequest — دالة مركزية لكل الطلبات
|
||||
// ───────────────────────────────────────────────────────────────
|
||||
// Retry logic للشبكات الضعيفة (سوريا):
|
||||
// • 3 محاولات لأخطاء الشبكة (SocketException / TimeoutException)
|
||||
// • انتظار 1 ثانية بين المحاولات لأخطاء SocketException
|
||||
// • بدون انتظار لأخطاء Timeout (نعيد فوراً)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
Future<dynamic> _makeRequest({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
required Map<String, String> headers,
|
||||
}) async {
|
||||
// timeouts مرتفعة مناسبة للإنترنت الضعيف في سوريا
|
||||
const totalTimeout = Duration(seconds: 60);
|
||||
|
||||
Future<http.Response> doPost() {
|
||||
final url = Uri.parse(link);
|
||||
return http
|
||||
.post(url, body: payload, headers: headers)
|
||||
.timeout(totalTimeout);
|
||||
}
|
||||
|
||||
http.Response? response;
|
||||
int attempts = 0;
|
||||
final requestId = DateTime.now().millisecondsSinceEpoch.toString().substring(7);
|
||||
|
||||
Log.print('🚀 [REQ-$requestId] $link');
|
||||
if (payload != null) Log.print('📦 [PAYLOAD-$requestId] $payload');
|
||||
|
||||
while (attempts < 3) {
|
||||
try {
|
||||
attempts++;
|
||||
response = await doPost();
|
||||
break; // نجح الاتصال — نخرج
|
||||
} on SocketException catch (_) {
|
||||
Log.print('⚠️ SocketException attempt $attempts — $link');
|
||||
if (attempts >= 3) {
|
||||
_netGuard.notifyOnce((title, msg) => mySnackeBarError(msg));
|
||||
return 'no_internet';
|
||||
}
|
||||
// انتظار قبل إعادة المحاولة — مهم للشبكات المتقطعة
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
} on TimeoutException catch (_) {
|
||||
Log.print('⚠️ TimeoutException attempt $attempts — $link');
|
||||
if (attempts >= 3) return 'failure';
|
||||
// لا انتظار — نعيد فوراً
|
||||
} catch (e) {
|
||||
// errno = 9 (Bad file descriptor) — إعادة المحاولة
|
||||
if (e.toString().contains('errno = 9') && attempts < 3) {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
continue;
|
||||
}
|
||||
addError(
|
||||
'HTTP Exception: $e', 'Try: $attempts', 'CRUD._makeRequest $link');
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
|
||||
// لو كل المحاولات فشلت بدون response
|
||||
if (response == null) return 'failure';
|
||||
|
||||
final sc = response.statusCode;
|
||||
final body = response.body;
|
||||
|
||||
Log.print('📥 [RES-$requestId] [$sc] $link');
|
||||
Log.print('📄 [BODY-$requestId] $body');
|
||||
|
||||
// 2xx
|
||||
if (sc >= 200 && sc < 300) {
|
||||
try {
|
||||
return jsonDecode(body);
|
||||
} catch (e, st) {
|
||||
addError(
|
||||
'JSON Decode Error', 'Body: $body\n$st', 'CRUD._makeRequest $link');
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
|
||||
// 401 → تجديد التوكن (مع حماية من الحلقة اللانهائية)
|
||||
if (sc == 401) {
|
||||
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
|
||||
final isNonCritical = link.contains('errorApp.php');
|
||||
if (!_isRefreshingJWT && !isNonCritical) {
|
||||
_isRefreshingJWT = true;
|
||||
try {
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
} finally {
|
||||
_isRefreshingJWT = false;
|
||||
}
|
||||
}
|
||||
return 'token_expired';
|
||||
}
|
||||
|
||||
// 5xx
|
||||
if (sc >= 500) {
|
||||
addError('Server 5xx', 'SC: $sc\nBody: $body', 'CRUD._makeRequest $link');
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
return 'failure';
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// post — طلب POST للسائق
|
||||
// التغيير: إضافة X-Device-FP header
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
Future<dynamic> post({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
|
||||
// فحص صلاحية التوكن قبل الإرسال — تجنب طلب مضمون الرفض
|
||||
if (!_isJwtValid(token) && !_isRefreshingJWT) {
|
||||
_isRefreshingJWT = true;
|
||||
try {
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
} finally {
|
||||
_isRefreshingJWT = false;
|
||||
}
|
||||
}
|
||||
|
||||
final headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
|
||||
};
|
||||
|
||||
return await _makeRequest(link: link, payload: payload, headers: headers);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// get — طلب GET للسائق (يستخدم POST method)
|
||||
// التغيير: إضافة X-Device-FP header + timeout مناسب لسوريا
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
Future<dynamic> get({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
try {
|
||||
// فحص صلاحية التوكن قبل الإرسال
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
if (!_isJwtValid(token) && !_isRefreshingJWT) {
|
||||
_isRefreshingJWT = true;
|
||||
try {
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
} finally {
|
||||
_isRefreshingJWT = false;
|
||||
}
|
||||
}
|
||||
|
||||
var url = Uri.parse(link);
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-FP': _getFpHeader(),
|
||||
},
|
||||
).timeout(const Duration(seconds: 60));
|
||||
|
||||
Log.print('get [$link]: ${response.statusCode}');
|
||||
Log.print('get body: ${response.body}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['status'] == 'success') return response.body;
|
||||
return jsonData['status'];
|
||||
} else if (response.statusCode == 401) {
|
||||
if (!_isRefreshingJWT) {
|
||||
_isRefreshingJWT = true;
|
||||
try {
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
} finally {
|
||||
_isRefreshingJWT = false;
|
||||
}
|
||||
}
|
||||
return 'token_expired';
|
||||
} else {
|
||||
addError('Non-200: ${response.statusCode}', 'crud().get - Other',
|
||||
url.toString());
|
||||
return 'failure';
|
||||
}
|
||||
} on TimeoutException {
|
||||
return 'failure';
|
||||
} on SocketException {
|
||||
return 'no_internet';
|
||||
} catch (e) {
|
||||
addError('GET Exception: $e', '', link);
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// postWallet — طلب POST للمحفظة
|
||||
// التغيير: إضافة X-Device-FP header
|
||||
// 3 headers: JWT + HMAC + FP
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
Future<dynamic> postWallet({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
var jwt = await LoginDriverController().getJwtWallet();
|
||||
final hmac = box.read(BoxName.hmac);
|
||||
|
||||
final headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Bearer $jwt',
|
||||
'X-HMAC-Auth': hmac.toString(),
|
||||
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
|
||||
};
|
||||
|
||||
return await _makeRequest(link: link, payload: payload, headers: headers);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// getWallet — طلب GET للمحفظة (يستخدم POST method)
|
||||
// التغيير: إضافة X-Device-FP header
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
Future<dynamic> getWallet({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
var s = await LoginDriverController().getJwtWallet();
|
||||
final hmac = box.read(BoxName.hmac);
|
||||
var url = Uri.parse(link);
|
||||
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Bearer $s',
|
||||
'X-HMAC-Auth': hmac.toString(),
|
||||
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
|
||||
},
|
||||
).timeout(const Duration(seconds: 60));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['status'] == 'success') return response.body;
|
||||
return jsonData['status'];
|
||||
} else if (response.statusCode == 401) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['error'] == 'Token expired') {
|
||||
await Get.put(LoginDriverController()).getJwtWallet();
|
||||
return 'token_expired';
|
||||
}
|
||||
addError('Unauthorized: ${jsonData['error']}', 'crud().getWallet - 401',
|
||||
url.toString());
|
||||
return 'failure';
|
||||
} else {
|
||||
addError('Non-200: ${response.statusCode}', 'crud().getWallet - Other',
|
||||
url.toString());
|
||||
return 'failure';
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// postWalletMtn — طلب MTN للمحفظة
|
||||
// التغيير: إضافة X-Device-FP header
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
Future<dynamic> postWalletMtn({
|
||||
required String link,
|
||||
Map<String, dynamic>? payload,
|
||||
}) async {
|
||||
final s = await LoginDriverController().getJwtWallet();
|
||||
final hmac = box.read(BoxName.hmac);
|
||||
final url = Uri.parse(link);
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Bearer $s',
|
||||
'X-HMAC-Auth': hmac.toString(),
|
||||
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
|
||||
},
|
||||
).timeout(const Duration(seconds: 60));
|
||||
|
||||
Map<String, dynamic> wrap(String status, {Object? message, int? code}) {
|
||||
return {
|
||||
'status': status,
|
||||
'message': message,
|
||||
'code': code ?? response.statusCode
|
||||
};
|
||||
}
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
try {
|
||||
return jsonDecode(response.body);
|
||||
} catch (e) {
|
||||
return wrap('failure',
|
||||
message: 'JSON decode error', code: response.statusCode);
|
||||
}
|
||||
} else if (response.statusCode == 401) {
|
||||
try {
|
||||
final jsonData = jsonDecode(response.body);
|
||||
if (jsonData is Map && jsonData['error'] == 'Token expired') {
|
||||
await Get.put(LoginDriverController()).getJWT();
|
||||
return {
|
||||
'status': 'failure',
|
||||
'message': 'token_expired',
|
||||
'code': 401
|
||||
};
|
||||
}
|
||||
return wrap('failure', message: jsonData);
|
||||
} catch (_) {
|
||||
return wrap('failure', message: response.body);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return wrap('failure', message: jsonDecode(response.body));
|
||||
} catch (_) {
|
||||
return wrap('failure', message: response.body);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
'status': 'failure',
|
||||
'message': 'HTTP request error: $e',
|
||||
'code': -1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// باقي الدوال الخارجية — لا تحتاج X-Device-FP (APIs خارجية)
|
||||
// =======================================================================
|
||||
|
||||
Future<dynamic> getAgoraToken({
|
||||
required String channelName,
|
||||
required String uid,
|
||||
}) async {
|
||||
var uid = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver);
|
||||
var res = await http.get(
|
||||
Uri.parse(
|
||||
'https://orca-app-b2i85.ondigitalocean.app/token?channelName=$channelName'),
|
||||
headers: {'Authorization': 'Bearer ${AK.agoraAppCertificate}'},
|
||||
);
|
||||
if (res.statusCode == 200) {
|
||||
return jsonDecode(res.body)['token'];
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> getLlama({
|
||||
required String link,
|
||||
required String payload,
|
||||
required String prompt,
|
||||
}) async {
|
||||
var url = Uri.parse(link);
|
||||
var headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization':
|
||||
'Bearer LL-X5lJ0Px9CzKK0HTuVZ3u2u4v3tGWkImLTG7okGRk4t25zrsLqJ0qNoUzZ2x4ciPy',
|
||||
};
|
||||
var data = json.encode({
|
||||
'model': 'Llama-3-70b-Inst-FW',
|
||||
'messages': [
|
||||
{
|
||||
'role': 'user',
|
||||
'content':
|
||||
'Extract the desired information from the following passage as json decoded like $prompt just in this:\n\n$payload',
|
||||
}
|
||||
],
|
||||
'temperature': 0.9,
|
||||
});
|
||||
var response = await http.post(url, body: data, headers: headers);
|
||||
if (response.statusCode == 200) return response.body;
|
||||
return response.statusCode;
|
||||
}
|
||||
|
||||
Future allMethodForAI(String prompt, linkPHP, imagePath) async {
|
||||
await ImageController().choosImage(linkPHP, imagePath);
|
||||
Future.delayed(const Duration(seconds: 2));
|
||||
var extractedString =
|
||||
await arabicTextExtractByVisionAndAI(imagePath: imagePath);
|
||||
var json = jsonDecode(extractedString);
|
||||
var textValues = extractTextFromLines(json);
|
||||
await Get.put(AI()).anthropicAI(textValues, prompt, imagePath);
|
||||
}
|
||||
|
||||
String extractTextFromLines(Map<String, dynamic> jsonData) {
|
||||
final readResult = jsonData['readResult'];
|
||||
final blocks = readResult['blocks'];
|
||||
final buffer = StringBuffer();
|
||||
for (final block in blocks) {
|
||||
for (final line in block['lines']) {
|
||||
buffer.write(line['text']);
|
||||
buffer.write('\n');
|
||||
}
|
||||
}
|
||||
return buffer.toString().trim();
|
||||
}
|
||||
|
||||
Future<dynamic> arabicTextExtractByVisionAndAI(
|
||||
{required String imagePath}) async {
|
||||
var headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Ocp-Apim-Subscription-Key': AK.ocpApimSubscriptionKey,
|
||||
};
|
||||
String imagePathFull =
|
||||
'${AppLink.server}/card_image/$imagePath-${box.read(BoxName.driverID)}.jpg';
|
||||
var request = http.Request(
|
||||
'POST',
|
||||
Uri.parse(
|
||||
'https://eastus.api.cognitive.microsoft.com/computervision/imageanalysis:analyze?features=caption,read&model-version=latest&language=en&api-version=2024-02-01'),
|
||||
);
|
||||
request.body = json.encode({'url': imagePathFull});
|
||||
request.headers.addAll(headers);
|
||||
http.StreamedResponse response = await request.send();
|
||||
if (response.statusCode == 200)
|
||||
return await response.stream.bytesToString();
|
||||
}
|
||||
|
||||
Future<dynamic> getChatGPT(
|
||||
{required String link, required String payload}) async {
|
||||
var url = Uri.parse(link);
|
||||
var headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ${Env.chatGPTkeySeferNew}',
|
||||
};
|
||||
var data = json.encode({
|
||||
'model': 'gpt-3.5-turbo',
|
||||
'messages': [
|
||||
{
|
||||
'role': 'user',
|
||||
'content':
|
||||
'Extract the desired information from the following passage as json decoded like vin,make,made,year,expiration_date,color,owner,registration_date just in this:\n\n$payload',
|
||||
}
|
||||
],
|
||||
'temperature': 0.9,
|
||||
});
|
||||
var response = await http.post(url, body: data, headers: headers);
|
||||
if (response.statusCode == 200) return response.body;
|
||||
return response.statusCode;
|
||||
}
|
||||
|
||||
|
||||
Future<dynamic> postPayMob(
|
||||
{required String link, Map<String, dynamic>? payload}) async {
|
||||
var url = Uri.parse(link);
|
||||
var response = await http.post(url,
|
||||
body: payload, headers: {'Content-Type': 'application/json'});
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (response.statusCode == 200) {
|
||||
if (jsonData['status'] == 'success') return response.body;
|
||||
return jsonData['status'];
|
||||
}
|
||||
return response.statusCode;
|
||||
}
|
||||
|
||||
// ── sendEmail — إصلاح: استخدام r() بدل X.r() القديم ─────────
|
||||
Future<void> sendEmail(String link, Map<String, String>? payload) async {
|
||||
// r() هي نفس دالة فك التشفير الثلاثي المختصرة
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
|
||||
if (!_isJwtValid(token)) {
|
||||
await LoginDriverController().getJWT();
|
||||
token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
}
|
||||
|
||||
final headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-FP': _getFpHeader(), // ← إثبات الجهاز
|
||||
};
|
||||
|
||||
final request = http.Request('POST', Uri.parse(link));
|
||||
request.bodyFields = payload ?? {};
|
||||
request.headers.addAll(headers);
|
||||
|
||||
final response = await request.send();
|
||||
if (response.statusCode != 200) {
|
||||
final responseBody = await response.stream.bytesToString();
|
||||
addError('sendEmail failed: ${response.statusCode}', responseBody,
|
||||
'CRUD.sendEmail');
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> postFromDialogue(
|
||||
{required String link, Map<String, dynamic>? payload}) async {
|
||||
var url = Uri.parse(link);
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: payload,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization':
|
||||
'Basic ${base64Encode(utf8.encode(AK.basicAuthCredentials))}',
|
||||
},
|
||||
);
|
||||
if (response.body.isNotEmpty) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (response.statusCode == 200 && jsonData['status'] == 'success') {
|
||||
Get.back();
|
||||
return response.body;
|
||||
}
|
||||
return jsonData['status'];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendVerificationRequest(String phoneNumber) async {
|
||||
final accountSid = AK.accountSIDTwillo;
|
||||
final authToken = AK.authTokenTwillo;
|
||||
final verifySid = AK.twilloRecoveryCode;
|
||||
|
||||
await http.post(
|
||||
Uri.parse(
|
||||
'https://verify.twilio.com/v2/Services/$verifySid/Verifications'),
|
||||
headers: {
|
||||
'Authorization':
|
||||
'Basic ' + base64Encode(utf8.encode('$accountSid:$authToken')),
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: {'To': phoneNumber, 'Channel': 'sms'},
|
||||
);
|
||||
}
|
||||
|
||||
Future<dynamic> getGoogleApi(
|
||||
{required String link, Map<String, dynamic>? payload}) async {
|
||||
var url = Uri.parse(link);
|
||||
var response = await http.post(url, body: payload);
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['status'] == 'OK') return jsonData;
|
||||
return jsonData['status'];
|
||||
}
|
||||
|
||||
Future<dynamic> update({
|
||||
required String endpoint,
|
||||
required Map<String, dynamic> data,
|
||||
required String id,
|
||||
}) async {
|
||||
var url = Uri.parse('$endpoint/$id');
|
||||
var response = await http.put(
|
||||
url,
|
||||
body: json.encode(data),
|
||||
headers: {
|
||||
'Authorization':
|
||||
'Basic ${base64Encode(utf8.encode(AK.basicAuthCredentials))}'
|
||||
},
|
||||
);
|
||||
return json.decode(response.body);
|
||||
}
|
||||
|
||||
Future<dynamic> delete({required String endpoint, required String id}) async {
|
||||
var url = Uri.parse('$endpoint/$id');
|
||||
var response = await http.delete(
|
||||
url,
|
||||
headers: {
|
||||
'Authorization':
|
||||
'Basic ${base64Encode(utf8.encode(AK.basicAuthCredentials))}'
|
||||
},
|
||||
);
|
||||
return json.decode(response.body);
|
||||
}
|
||||
|
||||
Future<dynamic> getMapSaas({
|
||||
required String link,
|
||||
}) async {
|
||||
var url = Uri.parse(link);
|
||||
try {
|
||||
var response = await http.get(
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': Env.mapSaasKey,
|
||||
},
|
||||
);
|
||||
Log.print('link -MapSaas: $link');
|
||||
Log.print('response -MapSaas: ${response.body}');
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Log.print('MapSaas Error: ${response.statusCode} - ${response.body}');
|
||||
return null;
|
||||
} catch (e) {
|
||||
Log.print('MapSaas Exception: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> postMapSaas({
|
||||
required String link,
|
||||
required Map<String, dynamic> payload,
|
||||
}) async {
|
||||
var url = Uri.parse(link);
|
||||
try {
|
||||
var response = await http.post(
|
||||
url,
|
||||
body: jsonEncode(payload),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': Env.mapSaasKey,
|
||||
},
|
||||
);
|
||||
Log.print('post -MapSaas link: $link');
|
||||
Log.print('post -MapSaas payload: $payload');
|
||||
Log.print('post -MapSaas response: ${response.body}');
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
Log.print('MapSaas Post Error: ${response.statusCode} - ${response.body}');
|
||||
return null;
|
||||
} catch (e) {
|
||||
Log.print('MapSaas Post Exception: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NoInternetException implements Exception {
|
||||
final String message;
|
||||
NoInternetException(
|
||||
[this.message =
|
||||
'No internet connection. Please check your network and try again.']);
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class WeakNetworkException implements Exception {
|
||||
final String message;
|
||||
WeakNetworkException(
|
||||
[this.message =
|
||||
'Your network connection is too slow. Please try again later.']);
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class ApiException implements Exception {
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
ApiException(this.message, [this.statusCode]);
|
||||
@override
|
||||
String toString() =>
|
||||
'ApiException: $message (Status Code: ${statusCode ?? 'N/A'})';
|
||||
}
|
||||
26
siro_driver/lib/controller/functions/custom_pant.dart
Executable file
26
siro_driver/lib/controller/functions/custom_pant.dart
Executable file
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class LineChartPainter extends CustomPainter {
|
||||
final List<double> data;
|
||||
|
||||
LineChartPainter(this.data);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
// Calculate the scale factor.
|
||||
final scaleFactor = size.height / 240;
|
||||
|
||||
// Draw the line chart.
|
||||
for (var i = 0; i < data.length - 1; i++) {
|
||||
final x1 = i * size.width / data.length;
|
||||
final y1 = data[i] * scaleFactor;
|
||||
final x2 = (i + 1) * size.width / data.length;
|
||||
final y2 = data[i + 1] * scaleFactor;
|
||||
|
||||
canvas.drawLine(Offset(x1, y1), Offset(x2, y2), Paint());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(LineChartPainter oldDelegate) => false;
|
||||
}
|
||||
205
siro_driver/lib/controller/functions/device_analyzer.dart
Normal file
205
siro_driver/lib/controller/functions/device_analyzer.dart
Normal file
@@ -0,0 +1,205 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'performance_test.dart'; // Make sure this path is correct
|
||||
|
||||
/// Analyzes various device hardware and software aspects to generate a compatibility score.
|
||||
/// This class provides a standardized output for the UI to consume easily.
|
||||
class DeviceAnalyzer {
|
||||
final DeviceInfoPlugin _deviceInfo = DeviceInfoPlugin();
|
||||
|
||||
/// Reads the total RAM from the system's meminfo file.
|
||||
/// Returns the value in Megabytes (MB).
|
||||
Future<double> _readTotalRamMB() async {
|
||||
try {
|
||||
final file = File('/proc/meminfo');
|
||||
if (!await file.exists()) return 0.0;
|
||||
final lines = await file.readAsLines();
|
||||
for (var line in lines) {
|
||||
if (line.startsWith('MemTotal')) {
|
||||
// Extracts the numeric value from the line.
|
||||
final kb = int.tryParse(RegExp(r'\d+').stringMatch(line) ?? '0') ?? 0;
|
||||
return kb / 1024.0; // Convert from Kilobytes to Megabytes
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Error reading total RAM: $e');
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/// Reads the current RAM usage percentage from the system's meminfo file.
|
||||
Future<double> _readUsedRamPercent() async {
|
||||
try {
|
||||
final file = File('/proc/meminfo');
|
||||
if (!await file.exists()) return 0.0;
|
||||
final lines = await file.readAsLines();
|
||||
int? total, available;
|
||||
for (var line in lines) {
|
||||
if (line.startsWith('MemTotal')) {
|
||||
total = int.tryParse(RegExp(r'\d+').stringMatch(line) ?? '');
|
||||
} else if (line.startsWith('MemAvailable')) {
|
||||
available = int.tryParse(RegExp(r'\d+').stringMatch(line) ?? '');
|
||||
}
|
||||
}
|
||||
if (total != null && available != null && total > 0) {
|
||||
final used = total - available;
|
||||
return (used / total) * 100.0;
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Error reading used RAM: $e');
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/// The main analysis function that runs all checks.
|
||||
Future<Map<String, dynamic>> analyzeDevice() async {
|
||||
List<Map<String, dynamic>> details = [];
|
||||
|
||||
if (!Platform.isAndroid) {
|
||||
return {
|
||||
'score': 0,
|
||||
'details': [
|
||||
{
|
||||
'label': 'النظام غير مدعوم',
|
||||
'status': false,
|
||||
'achieved_score': 0,
|
||||
'max_score': 100
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
final info = await _deviceInfo.androidInfo;
|
||||
final data = info.data;
|
||||
final features = List<String>.from(data['systemFeatures'] ?? []);
|
||||
|
||||
// 1. Android Version (Max: 10 points)
|
||||
final version =
|
||||
int.tryParse(info.version.release?.split('.').first ?? '0') ?? 0;
|
||||
final int androidScore = version >= 9 ? 10 : 0;
|
||||
details.add({
|
||||
'label': 'إصدار أندرويد ${info.version.release}',
|
||||
'status': androidScore > 0,
|
||||
'achieved_score': androidScore,
|
||||
'max_score': 10,
|
||||
});
|
||||
|
||||
// 2. Total RAM (Max: 10 points)
|
||||
final totalRam = await _readTotalRamMB();
|
||||
int ramScore;
|
||||
if (totalRam >= 8000) {
|
||||
ramScore = 10;
|
||||
} else if (totalRam >= 4000) {
|
||||
ramScore = 5;
|
||||
} else if (totalRam >= 3000) {
|
||||
ramScore = 3;
|
||||
} else {
|
||||
ramScore = 0;
|
||||
}
|
||||
details.add({
|
||||
'label': 'إجمالي الرام ${totalRam.toStringAsFixed(0)} ميجابايت',
|
||||
'status': ramScore >= 5,
|
||||
'achieved_score': ramScore,
|
||||
'max_score': 10,
|
||||
});
|
||||
|
||||
// 3. CPU Cores (Max: 10 points)
|
||||
final cores = Platform.numberOfProcessors;
|
||||
int coreScore = cores >= 6 ? 10 : (cores >= 4 ? 5 : 0);
|
||||
details.add({
|
||||
'label': 'أنوية المعالج ($cores)',
|
||||
'status': coreScore >= 5,
|
||||
'achieved_score': coreScore,
|
||||
'max_score': 10,
|
||||
});
|
||||
|
||||
// 4. Free Storage (Max: 5 points)
|
||||
final freeBytes = data['freeDiskSize'] ?? 0;
|
||||
final freeGB = freeBytes / (1024 * 1024 * 1024);
|
||||
int storeScore = freeGB >= 5 ? 5 : (freeGB >= 2 ? 3 : 0);
|
||||
details.add({
|
||||
'label': 'المساحة الحرة ${freeGB.toStringAsFixed(1)} جيجابايت',
|
||||
'status': storeScore >= 3,
|
||||
'achieved_score': storeScore,
|
||||
'max_score': 5,
|
||||
});
|
||||
|
||||
// 5. GPS + Gyroscope Sensors (Max: 10 points)
|
||||
bool okSensors = features.contains('android.hardware.location.gps') &&
|
||||
features.contains('android.hardware.sensor.gyroscope');
|
||||
final int sensorScore = okSensors ? 10 : 0;
|
||||
details.add({
|
||||
'label': 'حساسات GPS و Gyroscope',
|
||||
'status': okSensors,
|
||||
'achieved_score': sensorScore,
|
||||
'max_score': 10,
|
||||
});
|
||||
|
||||
// 6. Storage Write Speed (Max: 20 points)
|
||||
final writeSpeed = await PerformanceTester.testStorageWriteSpeed();
|
||||
int writeScore;
|
||||
if (writeSpeed >= 30) {
|
||||
writeScore = 20;
|
||||
} else if (writeSpeed >= 15) {
|
||||
writeScore = 15;
|
||||
} else if (writeSpeed >= 5) {
|
||||
writeScore = 10;
|
||||
} else {
|
||||
writeScore = 5;
|
||||
}
|
||||
details.add({
|
||||
'label': 'سرعة الكتابة (${writeSpeed.toStringAsFixed(1)} MB/s)',
|
||||
'status': writeScore >= 10,
|
||||
'achieved_score': writeScore,
|
||||
'max_score': 20,
|
||||
});
|
||||
|
||||
// 7. CPU Compute Speed (Max: 20 points)
|
||||
final cpuTime = await PerformanceTester.testCPUSpeed();
|
||||
int cpuScore;
|
||||
if (cpuTime <= 1.0) {
|
||||
cpuScore = 20;
|
||||
} else if (cpuTime <= 2.5) {
|
||||
cpuScore = 15;
|
||||
} else if (cpuTime <= 4.0) {
|
||||
cpuScore = 10;
|
||||
} else {
|
||||
cpuScore = 5;
|
||||
}
|
||||
details.add({
|
||||
'label': 'سرعة المعالجة (${cpuTime.toStringAsFixed(2)} ثانية)',
|
||||
'status': cpuScore >= 10,
|
||||
'achieved_score': cpuScore,
|
||||
'max_score': 20,
|
||||
});
|
||||
|
||||
// 8. Memory Pressure (Max: 15 points)
|
||||
final usedPercent = await _readUsedRamPercent();
|
||||
int memScore;
|
||||
if (usedPercent <= 60) {
|
||||
memScore = 15;
|
||||
} else if (usedPercent <= 80) {
|
||||
memScore = 10;
|
||||
} else if (usedPercent <= 90) {
|
||||
memScore = 5;
|
||||
} else {
|
||||
memScore = 0;
|
||||
}
|
||||
details.add({
|
||||
'label': 'استخدام الرام الحالي (${usedPercent.toStringAsFixed(0)}%)',
|
||||
'status': memScore >= 10,
|
||||
'achieved_score': memScore,
|
||||
'max_score': 15,
|
||||
});
|
||||
|
||||
// Calculate the final total score by summing up the achieved scores.
|
||||
final totalScore = details.fold<int>(
|
||||
0, (sum, item) => sum + (item['achieved_score'] as int));
|
||||
|
||||
return {
|
||||
'score': totalScore.clamp(0, 100),
|
||||
'details': details,
|
||||
};
|
||||
}
|
||||
}
|
||||
80
siro_driver/lib/controller/functions/device_info.dart
Executable file
80
siro_driver/lib/controller/functions/device_info.dart
Executable file
@@ -0,0 +1,80 @@
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import '../../constant/box_name.dart';
|
||||
|
||||
class DeviceInfo {
|
||||
final String? manufacturer;
|
||||
final String? model;
|
||||
final String? deviceId;
|
||||
final String? osVersion;
|
||||
final String? platform;
|
||||
final String? deviceName;
|
||||
final bool? isPhysicalDevice;
|
||||
|
||||
DeviceInfo({
|
||||
this.manufacturer,
|
||||
this.model,
|
||||
this.deviceId,
|
||||
this.osVersion,
|
||||
this.platform,
|
||||
this.deviceName,
|
||||
this.isPhysicalDevice,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'manufacturer': manufacturer,
|
||||
'model': model,
|
||||
'deviceId': deviceId,
|
||||
'osVersion': osVersion,
|
||||
'platform': platform,
|
||||
'deviceName': deviceName,
|
||||
'isPhysicalDevice': isPhysicalDevice,
|
||||
};
|
||||
}
|
||||
|
||||
class DeviceController {
|
||||
final box = GetStorage();
|
||||
final _deviceInfo = DeviceInfoPlugin();
|
||||
|
||||
Future<DeviceInfo> getDeviceInfo() async {
|
||||
if (Platform.isAndroid) {
|
||||
return await _getAndroidDeviceInfo();
|
||||
} else if (Platform.isIOS) {
|
||||
return await _getIosDeviceInfo();
|
||||
}
|
||||
throw UnsupportedError('Unsupported platform');
|
||||
}
|
||||
|
||||
Future<DeviceInfo> _getAndroidDeviceInfo() async {
|
||||
final androidInfo = await _deviceInfo.androidInfo;
|
||||
final deviceInfo = DeviceInfo(
|
||||
manufacturer: androidInfo.manufacturer,
|
||||
model: androidInfo.model,
|
||||
deviceId: androidInfo.id,
|
||||
osVersion: androidInfo.version.release,
|
||||
platform: 'Android',
|
||||
deviceName: androidInfo.device,
|
||||
isPhysicalDevice: androidInfo.isPhysicalDevice,
|
||||
);
|
||||
|
||||
box.write(BoxName.deviceInfo, deviceInfo.toJson());
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
Future<DeviceInfo> _getIosDeviceInfo() async {
|
||||
final iosInfo = await _deviceInfo.iosInfo;
|
||||
final deviceInfo = DeviceInfo(
|
||||
manufacturer: 'Apple',
|
||||
model: iosInfo.model,
|
||||
deviceId: iosInfo.identifierForVendor,
|
||||
osVersion: iosInfo.systemVersion,
|
||||
platform: 'iOS',
|
||||
deviceName: iosInfo.name,
|
||||
isPhysicalDevice: iosInfo.isPhysicalDevice,
|
||||
);
|
||||
|
||||
box.write(BoxName.deviceInfo, deviceInfo.toJson());
|
||||
return deviceInfo;
|
||||
}
|
||||
}
|
||||
42
siro_driver/lib/controller/functions/digit_obsecur_formate.dart
Executable file
42
siro_driver/lib/controller/functions/digit_obsecur_formate.dart
Executable file
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class DigitObscuringFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue, TextEditingValue newValue) {
|
||||
final maskedText = maskDigits(newValue.text);
|
||||
return newValue.copyWith(
|
||||
text: maskedText,
|
||||
selection: updateCursorPosition(maskedText, newValue.selection));
|
||||
}
|
||||
|
||||
String maskDigits(String text) {
|
||||
final totalDigits = text.length;
|
||||
final visibleDigits = 4;
|
||||
final hiddenDigits = totalDigits - visibleDigits * 2;
|
||||
|
||||
final firstVisibleDigits = text.substring(0, visibleDigits);
|
||||
final lastVisibleDigits = text.substring(totalDigits - visibleDigits);
|
||||
|
||||
final maskedDigits = List.filled(hiddenDigits, '*').join();
|
||||
|
||||
return '$firstVisibleDigits$maskedDigits$lastVisibleDigits';
|
||||
}
|
||||
|
||||
TextSelection updateCursorPosition(
|
||||
String maskedText, TextSelection currentSelection) {
|
||||
final cursorPosition = currentSelection.baseOffset;
|
||||
final cursorOffset =
|
||||
currentSelection.extentOffset - currentSelection.baseOffset;
|
||||
final totalDigits = maskedText.length;
|
||||
const visibleDigits = 4;
|
||||
final hiddenDigits = totalDigits - visibleDigits * 2;
|
||||
|
||||
final updatedPosition = cursorPosition <= visibleDigits
|
||||
? cursorPosition
|
||||
: hiddenDigits + visibleDigits + (cursorPosition - visibleDigits);
|
||||
|
||||
return TextSelection.collapsed(
|
||||
offset: updatedPosition, affinity: currentSelection.affinity);
|
||||
}
|
||||
}
|
||||
41
siro_driver/lib/controller/functions/document_scanner.dart
Executable file
41
siro_driver/lib/controller/functions/document_scanner.dart
Executable file
@@ -0,0 +1,41 @@
|
||||
// import 'dart:io';
|
||||
//
|
||||
// import 'package:get/get.dart';
|
||||
// import 'package:image_picker/image_picker.dart';
|
||||
// import 'package:google_ml_kit/google_ml_kit.dart';
|
||||
//
|
||||
// class ImagePickerController extends GetxController {
|
||||
// RxBool textScanning = false.obs;
|
||||
// RxString scannedText = ''.obs;
|
||||
//
|
||||
// Future<void> getImage(ImageSource source) async {
|
||||
// try {
|
||||
// final pickedImage = await ImagePicker().pickImage(source: source);
|
||||
// if (pickedImage != null) {
|
||||
// textScanning.value = true;
|
||||
// final imageFile = File(pickedImage.path);
|
||||
// getRecognisedText(imageFile);
|
||||
// }
|
||||
// } catch (e) {
|
||||
// textScanning.value = false;
|
||||
// scannedText.value = "Error occurred while scanning";
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Future<void> getRecognisedText(File image) async {
|
||||
// final inputImage = InputImage.fromFilePath(image.path);
|
||||
// final textDetector = GoogleMlKit.vision.textRecognizer();
|
||||
// final RecognizedText recognisedText =
|
||||
// await textDetector.processImage(inputImage);
|
||||
// await textDetector.close();
|
||||
//
|
||||
// scannedText.value = '';
|
||||
// for (TextBlock block in recognisedText.blocks) {
|
||||
// for (TextLine line in block.lines) {
|
||||
// scannedText.value += line.text + '\n';
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// textScanning.value = false;
|
||||
// }
|
||||
// }
|
||||
32
siro_driver/lib/controller/functions/encrypt.dart
Executable file
32
siro_driver/lib/controller/functions/encrypt.dart
Executable file
@@ -0,0 +1,32 @@
|
||||
import 'dart:convert';
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
|
||||
import '../../constant/api_key.dart';
|
||||
|
||||
class KeyEncryption {
|
||||
// استخدم مفتاح بطول 32 حرفًا
|
||||
static final _key = encrypt.Key.fromUtf8(AK.keyOfApp);
|
||||
static final _iv =
|
||||
encrypt.IV.fromLength(16); // توليد تهيئة عشوائية بطول 16 بايت
|
||||
|
||||
static String encryptKey(String key) {
|
||||
final encrypter =
|
||||
encrypt.Encrypter(encrypt.AES(_key, mode: encrypt.AESMode.cbc));
|
||||
final encrypted = encrypter.encrypt(key, iv: _iv);
|
||||
final result = _iv.bytes + encrypted.bytes; // تضمين التهيئة مع النص المشفر
|
||||
return base64Encode(result);
|
||||
}
|
||||
|
||||
static String decryptKey(String encryptedKey) {
|
||||
print('encryptedKey: ${AK.keyOfApp}');
|
||||
|
||||
final decoded = base64Decode(encryptedKey);
|
||||
print('encryptedKey: $encryptedKey');
|
||||
final iv = encrypt.IV(decoded.sublist(0, 16)); // استخراج التهيئة
|
||||
final encrypted =
|
||||
encrypt.Encrypted(decoded.sublist(16)); // استخراج النص المشفر
|
||||
final encrypter =
|
||||
encrypt.Encrypter(encrypt.AES(_key, mode: encrypt.AESMode.cbc));
|
||||
return encrypter.decrypt(encrypted, iv: iv);
|
||||
}
|
||||
}
|
||||
77
siro_driver/lib/controller/functions/encrypt_decrypt.dart
Executable file
77
siro_driver/lib/controller/functions/encrypt_decrypt.dart
Executable file
@@ -0,0 +1,77 @@
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:secure_string_operations/secure_string_operations.dart';
|
||||
|
||||
import '../../constant/char_map.dart';
|
||||
import '../../env/env.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
|
||||
class EncryptionHelper {
|
||||
static EncryptionHelper? _instance;
|
||||
|
||||
late final encrypt.Key key;
|
||||
late final encrypt.IV iv;
|
||||
|
||||
EncryptionHelper._(this.key, this.iv);
|
||||
static EncryptionHelper get instance {
|
||||
if (_instance == null) {
|
||||
throw Exception(
|
||||
"EncryptionHelper is not initialized. Call `await EncryptionHelper.initialize()` in main.");
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
/// Initializes and stores the instance globally
|
||||
static Future<void> initialize() async {
|
||||
if (_instance != null) {
|
||||
debugPrint("EncryptionHelper is already initialized.");
|
||||
return; // Prevent re-initialization
|
||||
}
|
||||
debugPrint("Initializing EncryptionHelper...");
|
||||
var keyOfApp = r(Env.keyOfApp).toString().split(Env.addd)[0];
|
||||
var initializationVector =
|
||||
r(Env.initializationVector).toString().split(Env.addd)[0];
|
||||
|
||||
// Set the global instance
|
||||
_instance = EncryptionHelper._(
|
||||
encrypt.Key.fromUtf8(keyOfApp!),
|
||||
encrypt.IV.fromUtf8(initializationVector!),
|
||||
);
|
||||
debugPrint("EncryptionHelper initialized successfully.");
|
||||
}
|
||||
|
||||
/// Encrypts a string
|
||||
String encryptData(String plainText) {
|
||||
try {
|
||||
final encrypter =
|
||||
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
|
||||
final encrypted = encrypter.encrypt(plainText, iv: iv);
|
||||
return encrypted.base64;
|
||||
} catch (e) {
|
||||
debugPrint('Encryption Error: $e');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypts a string
|
||||
String decryptData(String encryptedText) {
|
||||
try {
|
||||
final encrypter =
|
||||
encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
|
||||
final encrypted = encrypt.Encrypted.fromBase64(encryptedText);
|
||||
return encrypter.decrypt(encrypted, iv: iv);
|
||||
} catch (e) {
|
||||
debugPrint('Decryption Error: $e');
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r(String string) {
|
||||
return X.r(X.r(X.r(string, cn), cC), cs).toString();
|
||||
}
|
||||
|
||||
c(String string) {
|
||||
return X.c(X.c(X.c(string, cn), cC), cs).toString();
|
||||
}
|
||||
95
siro_driver/lib/controller/functions/face_detect.dart
Executable file
95
siro_driver/lib/controller/functions/face_detect.dart
Executable file
@@ -0,0 +1,95 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/io_client.dart';
|
||||
|
||||
import '../../constant/links.dart';
|
||||
import 'encrypt_decrypt.dart';
|
||||
import 'upload_image.dart';
|
||||
|
||||
Future<String> faceDetector() async {
|
||||
await ImageController().choosFace(AppLink.uploadEgypt, 'face_detect');
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
|
||||
var headers = {
|
||||
// 'Authorization': 'Basic ${AK.basicCompareFaces}',
|
||||
'Authorization': 'Basic hamza:12345678',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
// var request = http.Request('POST', Uri.parse(//Todo
|
||||
// 'https://face-detect-f6924392c4c7.herokuapp.com/compare_faces'));
|
||||
|
||||
var request = http.Request(
|
||||
'POST', Uri.parse('https://mohkh.online:5000/compare_faces'));
|
||||
|
||||
request.body = json.encode({
|
||||
"url1":
|
||||
"${AppLink.seferCairoServer}/card_image/id_front-${(box.read(BoxName.driverID))}.jpg",
|
||||
"url2":
|
||||
"https://api.sefer.live/sefer/card_image/face_detect-${(box.read(BoxName.driverID))}.jpg"
|
||||
});
|
||||
print('request.body: ${request.body}');
|
||||
request.headers.addAll(headers);
|
||||
|
||||
try {
|
||||
http.Client client = await createHttpClient();
|
||||
http.StreamedResponse response = await client.send(request);
|
||||
// http.StreamedResponse response = await request.send();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
String result = await response.stream.bytesToString();
|
||||
print('result: ${result}');
|
||||
return result;
|
||||
} else {
|
||||
print('Error: ${response.reasonPhrase}');
|
||||
return 'Error: ${response.reasonPhrase}';
|
||||
}
|
||||
} catch (e) {
|
||||
print('Exception occurred: $e');
|
||||
return 'Error: $e';
|
||||
}
|
||||
}
|
||||
|
||||
Future<http.Client> createHttpClient() async {
|
||||
final SecurityContext securityContext = SecurityContext();
|
||||
HttpClient httpClient = HttpClient(context: securityContext);
|
||||
httpClient.badCertificateCallback =
|
||||
(X509Certificate cert, String host, int port) => true; // Bypass SSL
|
||||
return IOClient(httpClient);
|
||||
}
|
||||
|
||||
Future<String> faceDetector2(String url1, String url2) async {
|
||||
var headers = {
|
||||
'Authorization': 'Basic hamza:12345678',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
var request = http.Request(
|
||||
'POST', Uri.parse('https://mohkh.online:5000/compare_faces'));
|
||||
|
||||
request.body = json.encode({"url1": url1, "url2": url2});
|
||||
request.headers.addAll(headers);
|
||||
|
||||
try {
|
||||
http.Client client = await createHttpClient(); // Use custom client
|
||||
DateTime startTime = DateTime.now();
|
||||
http.StreamedResponse response = await client.send(request);
|
||||
DateTime endTime = DateTime.now();
|
||||
Duration duration = endTime.difference(startTime);
|
||||
if (response.statusCode == 200) {
|
||||
print(await response.stream.bytesToString());
|
||||
print(duration.inSeconds);
|
||||
|
||||
return await response.stream.bytesToString();
|
||||
} else {
|
||||
print(await response.stream.bytesToString());
|
||||
return 'Error: ${response.reasonPhrase}';
|
||||
}
|
||||
} catch (e) {
|
||||
return 'Exception: $e';
|
||||
}
|
||||
}
|
||||
1639
siro_driver/lib/controller/functions/gemeni.dart
Executable file
1639
siro_driver/lib/controller/functions/gemeni.dart
Executable file
File diff suppressed because it is too large
Load Diff
34
siro_driver/lib/controller/functions/geolocation.dart
Executable file
34
siro_driver/lib/controller/functions/geolocation.dart
Executable file
@@ -0,0 +1,34 @@
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
class GeoLocation {
|
||||
Future<Position> getCurrentLocation() async {
|
||||
bool serviceEnabled;
|
||||
LocationPermission permission;
|
||||
|
||||
// Check if location services are enabled.
|
||||
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
// Location services are not enabled, so we request the user to enable it.
|
||||
return Future.error('Location services are disabled.');
|
||||
}
|
||||
|
||||
permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
// Permissions are denied, we cannot fetch the location.
|
||||
return Future.error('Location permissions are denied');
|
||||
}
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
// Permissions are denied forever, we cannot request permissions.
|
||||
return Future.error(
|
||||
'Location permissions are permanently denied, we cannot request permissions.');
|
||||
}
|
||||
|
||||
// When we reach here, permissions are granted and we can fetch the location.
|
||||
return await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
}
|
||||
}
|
||||
123
siro_driver/lib/controller/functions/launch.dart
Executable file
123
siro_driver/lib/controller/functions/launch.dart
Executable file
@@ -0,0 +1,123 @@
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'dart:io';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
|
||||
void showInBrowser(String url) async {
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
launchUrl(Uri.parse(url));
|
||||
} else {}
|
||||
}
|
||||
|
||||
String cleanAndFormatPhoneNumber(String phoneNumber) {
|
||||
// 1. Clean the number
|
||||
String formattedNumber = phoneNumber.replaceAll(RegExp(r'\s+'), '');
|
||||
|
||||
// 2. Format logic (Syria/Egypt/International)
|
||||
if (formattedNumber.length > 6) {
|
||||
if (formattedNumber.startsWith('09')) {
|
||||
formattedNumber = '+963${formattedNumber.substring(1)}';
|
||||
} else if (formattedNumber.startsWith('01') && formattedNumber.length == 11) {
|
||||
formattedNumber = '+20${formattedNumber.substring(1)}';
|
||||
} else if (formattedNumber.startsWith('00')) {
|
||||
formattedNumber = '+${formattedNumber.substring(2)}';
|
||||
} else if (!formattedNumber.startsWith('+')) {
|
||||
formattedNumber = '+$formattedNumber';
|
||||
}
|
||||
}
|
||||
return formattedNumber;
|
||||
}
|
||||
|
||||
Future<void> makePhoneCall(String phoneNumber) async {
|
||||
String formattedNumber = cleanAndFormatPhoneNumber(phoneNumber);
|
||||
|
||||
if (!formattedNumber.startsWith('+963')) {
|
||||
mySnackeBarError("Calling non-Syrian numbers is not supported".tr);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create URI directly from String to avoid double encoding '+' as '%2B'
|
||||
final Uri launchUri = Uri.parse('tel:$formattedNumber');
|
||||
|
||||
// 4. Execute with externalApplication mode
|
||||
try {
|
||||
if (!await launchUrl(launchUri, mode: LaunchMode.externalApplication)) {
|
||||
throw 'Could not launch $launchUri';
|
||||
}
|
||||
} catch (e) {
|
||||
if (await canLaunchUrl(launchUri)) {
|
||||
await launchUrl(launchUri);
|
||||
} else {
|
||||
print("Cannot launch url: $launchUri");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void launchCommunication(
|
||||
String method, String contactInfo, String message) async {
|
||||
String formattedContact = cleanAndFormatPhoneNumber(contactInfo);
|
||||
// WhatsApp prefers the phone number without the '+' prefix
|
||||
String whatsappContact = formattedContact.replaceAll('+', '');
|
||||
String url;
|
||||
|
||||
if (Platform.isIOS) {
|
||||
switch (method) {
|
||||
case 'phone':
|
||||
if (!formattedContact.startsWith('+963')) {
|
||||
mySnackeBarError("Calling non-Syrian numbers is not supported".tr);
|
||||
return;
|
||||
}
|
||||
url = 'tel:$formattedContact';
|
||||
break;
|
||||
case 'sms':
|
||||
url = 'sms:$formattedContact?body=${Uri.encodeComponent(message)}';
|
||||
break;
|
||||
case 'whatsapp':
|
||||
url =
|
||||
'https://api.whatsapp.com/send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
|
||||
break;
|
||||
case 'email':
|
||||
url =
|
||||
'mailto:$formattedContact?subject=Subject&body=${Uri.encodeComponent(message)}';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
} else if (Platform.isAndroid) {
|
||||
switch (method) {
|
||||
case 'phone':
|
||||
if (!formattedContact.startsWith('+963')) {
|
||||
mySnackeBarError("Calling non-Syrian numbers is not supported".tr);
|
||||
return;
|
||||
}
|
||||
url = 'tel:$formattedContact';
|
||||
break;
|
||||
case 'sms':
|
||||
url = 'sms:$formattedContact?body=${Uri.encodeComponent(message)}';
|
||||
break;
|
||||
case 'whatsapp':
|
||||
final bool whatsappInstalled =
|
||||
await canLaunchUrl(Uri.parse('whatsapp://'));
|
||||
if (whatsappInstalled) {
|
||||
url =
|
||||
'whatsapp://send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
|
||||
} else {
|
||||
url =
|
||||
'https://api.whatsapp.com/send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
|
||||
}
|
||||
break;
|
||||
case 'email':
|
||||
url =
|
||||
'mailto:$formattedContact?subject=Subject&body=${Uri.encodeComponent(message)}';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
await launchUrl(Uri.parse(url));
|
||||
} else {}
|
||||
}
|
||||
37
siro_driver/lib/controller/functions/llama_ai.dart
Executable file
37
siro_driver/lib/controller/functions/llama_ai.dart
Executable file
@@ -0,0 +1,37 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/controller/functions/gemeni.dart';
|
||||
|
||||
class LlamaAi {
|
||||
Future<Map> getCarRegistrationData(String input, prompt) async {
|
||||
Map exrtatDataFinal = {};
|
||||
String oneLine = input.replaceAll('\n', ' ');
|
||||
// var res = await CRUD().getLlama(link: AppLink.gemini, payload: oneLine);
|
||||
var res = await CRUD()
|
||||
.getLlama(link: AppLink.llama, payload: oneLine, prompt: prompt);
|
||||
|
||||
var decod = jsonDecode(res.toString());
|
||||
// exrtatDataFinal = jsonDecode(extractDataFromJsonString(decod['choices']));
|
||||
extractDataFromJsonString(decod['choices'][0]['message']['content']);
|
||||
return exrtatDataFinal;
|
||||
}
|
||||
|
||||
String extractDataFromJsonString(String jsonString) {
|
||||
// Remove any leading or trailing whitespace from the string
|
||||
jsonString = jsonString.trim();
|
||||
|
||||
// Extract the JSON substring from the given string
|
||||
final startIndex = jsonString.indexOf('{');
|
||||
final endIndex = jsonString.lastIndexOf('}');
|
||||
final jsonSubstring = jsonString.substring(startIndex, endIndex + 1);
|
||||
|
||||
// Parse the JSON substring into a Map
|
||||
final jsonData = jsonDecode(jsonSubstring);
|
||||
|
||||
// Return the extracted data
|
||||
|
||||
return jsonEncode(jsonData);
|
||||
}
|
||||
}
|
||||
85
siro_driver/lib/controller/functions/location_background_controller.dart
Executable file
85
siro_driver/lib/controller/functions/location_background_controller.dart
Executable file
@@ -0,0 +1,85 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:get/get_core/src/get_main.dart';
|
||||
import 'package:get/get_navigation/src/extension_navigation.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
|
||||
import 'background_service.dart';
|
||||
|
||||
Future<void> requestNotificationPermission() async {
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
if (androidInfo.version.sdkInt >= 33) {
|
||||
// Android 13+
|
||||
final status = await Permission.notification.request();
|
||||
if (!status.isGranted) {
|
||||
print('⚠️ إذن الإشعارات مرفوض');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// بعد الحصول على الإذن، ابدأ الخدمة
|
||||
await BackgroundServiceHelper.startService();
|
||||
}
|
||||
|
||||
class PermissionsHelper {
|
||||
/// طلب إذن الإشعارات على Android 13+
|
||||
static Future<bool> requestNotificationPermission() async {
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
|
||||
if (androidInfo.version.sdkInt >= 33) {
|
||||
final status = await Permission.notification.request();
|
||||
|
||||
if (status.isDenied) {
|
||||
print('⚠️ إذن الإشعارات مرفوض');
|
||||
mySnackbarWarning(
|
||||
"يرجى منح صلاحية الإشعارات لضمان وصول الطلبات إليك");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status.isPermanentlyDenied) {
|
||||
print('⚠️ إذن الإشعارات مرفوض بشكل دائم - افتح الإعدادات');
|
||||
mySnackbarWarning('يرجى فتح الإعدادات وتفعيل صلاحية الإشعارات');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// طلب جميع الإذونات المطلوبة
|
||||
static Future<bool> requestAllPermissions() async {
|
||||
// إذن الإشعارات (اختياري)
|
||||
await requestNotificationPermission();
|
||||
|
||||
// 1. طلب إذن الموقع الأساسي فقط إذا كان مرفوضاً
|
||||
var status = await Permission.location.status;
|
||||
if (status.isDenied) {
|
||||
status = await Permission.location.request();
|
||||
}
|
||||
|
||||
if (status.isPermanentlyDenied) {
|
||||
_showSettingsDialog('الموقع');
|
||||
return false;
|
||||
}
|
||||
|
||||
return status.isGranted || status.isLimited;
|
||||
}
|
||||
|
||||
static void _showSettingsDialog(String permissionName) {
|
||||
MyDialog().getDialog(
|
||||
'صلاحية $permissionName مطلوبة',
|
||||
'لقد قمت برفض صلاحية $permissionName سابقاً. يرجى تفعيلها من الإعدادات لتمكين التطبيق من العمل.',
|
||||
() async {
|
||||
await openAppSettings();
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
820
siro_driver/lib/controller/functions/location_controller.dart
Executable file
820
siro_driver/lib/controller/functions/location_controller.dart
Executable file
@@ -0,0 +1,820 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:geolocator/geolocator.dart' as geo;
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:location/location.dart';
|
||||
import 'package:battery_plus/battery_plus.dart';
|
||||
import 'package:permission_handler/permission_handler.dart' as ph;
|
||||
import 'package:socket_io_client/socket_io_client.dart' as IO;
|
||||
import 'package:siro_driver/constant/table_names.dart';
|
||||
import 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../firebase/local_notification.dart';
|
||||
import '../home/captin/home_captain_controller.dart';
|
||||
import '../home/captin/map_driver_controller.dart';
|
||||
import '../home/payment/captain_wallet_controller.dart';
|
||||
import 'background_service.dart';
|
||||
import 'crud.dart';
|
||||
|
||||
class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
// ===================================================================
|
||||
// ====== Tunables ======
|
||||
// ===================================================================
|
||||
static const Duration recordIntervalNormal = Duration(seconds: 3);
|
||||
static const Duration uploadBatchIntervalNormal = Duration(minutes: 2);
|
||||
static const Duration recordIntervalPowerSave = Duration(seconds: 10);
|
||||
static const Duration uploadBatchIntervalPowerSave = Duration(minutes: 5);
|
||||
|
||||
static const double lowWalletThreshold = -200;
|
||||
static const int powerSaveTriggerLevel = 20;
|
||||
static const int powerSaveExitLevel = 25;
|
||||
|
||||
// ===================================================================
|
||||
// ====== Services & Variables ======
|
||||
// ===================================================================
|
||||
late final Location location = Location();
|
||||
final Battery _battery = Battery();
|
||||
|
||||
IO.Socket? socket;
|
||||
bool isSocketConnected = false;
|
||||
Timer? _socketHeartbeat;
|
||||
|
||||
StreamSubscription<LocationData>? _locSub;
|
||||
StreamSubscription<BatteryState>? _batterySub;
|
||||
|
||||
Timer? _recordTimer;
|
||||
Timer? _uploadBatchTimer;
|
||||
|
||||
late final HomeCaptainController _homeCtrl;
|
||||
late final CaptainWalletController _walletCtrl;
|
||||
|
||||
LatLng myLocation = LatLng(
|
||||
box.read('last_lat') ?? 0.0,
|
||||
box.read('last_lng') ?? 0.0,
|
||||
);
|
||||
double heading = box.read('last_heading') ?? 0.0;
|
||||
double speed = 0.0;
|
||||
double totalDistance = 0.0;
|
||||
bool _isReady = false;
|
||||
bool _isPowerSavingMode = false;
|
||||
|
||||
final List<Map<String, dynamic>> _trackBuffer = [];
|
||||
final List<Map<String, dynamic>> _behaviorBuffer = [];
|
||||
|
||||
LatLng? _lastPosForDistance;
|
||||
LatLng? _lastRecordedRealLoc;
|
||||
DateTime? _lastRecordedTime;
|
||||
|
||||
LatLng? _lastSqlLoc;
|
||||
double? _lastSpeed;
|
||||
DateTime? _lastSpeedAt;
|
||||
|
||||
@override
|
||||
Future<void> onInit() async {
|
||||
super.onInit();
|
||||
Log.print('🚀 LocationController Starting...');
|
||||
|
||||
// 1. Register Lifecycle Observer
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
box.write(BoxName.isAppInForeground, true);
|
||||
|
||||
// مراقب الحالة (Status Watcher)
|
||||
box.listenKey(BoxName.statusDriverLocation, (value) {
|
||||
if (value == 'blocked') {
|
||||
Log.print("⛔ Driver is Blocked: Force Stopping Location Updates.");
|
||||
stopLocationUpdates();
|
||||
if (socket != null && socket!.connected) {
|
||||
socket!.emit('update_location', {
|
||||
'driver_id': box.read(BoxName.driverID),
|
||||
'status': 'blocked',
|
||||
'lat': myLocation.latitude,
|
||||
'lng': myLocation.longitude,
|
||||
'heading': heading,
|
||||
'speed': speed * 3.6,
|
||||
'distance': totalDistance
|
||||
});
|
||||
socket!.disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bool deps = await _awaitDependencies();
|
||||
if (!deps) return;
|
||||
|
||||
_isReady = true;
|
||||
|
||||
initSocket();
|
||||
await _initLocationSettings();
|
||||
_listenToBatteryChanges();
|
||||
|
||||
if (box.read(BoxName.statusDriverLocation) != 'blocked') {
|
||||
await startLocationUpdates();
|
||||
}
|
||||
|
||||
Log.print('✅ LocationController Initialized.');
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
box.write(BoxName.isAppInForeground, false);
|
||||
stopLocationUpdates();
|
||||
_batterySub?.cancel();
|
||||
_stopHeartbeat();
|
||||
socket?.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 🔥 Lifecycle Manager (Fixes Freeze & Background issues)
|
||||
// ===================================================================
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
Log.print("📱 Lifecycle: App is in FOREGROUND");
|
||||
box.write(BoxName.isAppInForeground, true);
|
||||
|
||||
// إيقاف خدمة الخلفية
|
||||
BackgroundServiceHelper.stopService();
|
||||
|
||||
if (socket == null || (!socket!.connected && !_isInitializingSocket)) {
|
||||
Log.print("🔄 Initializing Socket on resume...");
|
||||
initSocket();
|
||||
}
|
||||
} else if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.detached) {
|
||||
Log.print("📱 Lifecycle: App is in BACKGROUND");
|
||||
box.write(BoxName.isAppInForeground, false);
|
||||
|
||||
// تشغيل خدمة الخلفية للأندرويد لضمان بقاء التطبيق حياً
|
||||
if (!Platform.isIOS) {
|
||||
BackgroundServiceHelper.startService();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _awaitDependencies() async {
|
||||
int attempts = 0;
|
||||
while (attempts < 10) {
|
||||
if (Get.isRegistered<HomeCaptainController>() &&
|
||||
Get.isRegistered<CaptainWalletController>()) {
|
||||
_homeCtrl = Get.find<HomeCaptainController>();
|
||||
_walletCtrl = Get.find<CaptainWalletController>();
|
||||
return true;
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
attempts++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// ====== Socket Logic (Improved) ======
|
||||
// ===================================================================
|
||||
|
||||
bool _isInitializingSocket = false;
|
||||
|
||||
void initSocket() {
|
||||
// منع الاستدعاءات المتداخلة التي تسبب قتل الاتصال قبل اكتماله
|
||||
if (_isInitializingSocket) {
|
||||
Log.print("⏳ Socket is already initializing. Skipping redundant call.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (socket != null && socket!.connected) {
|
||||
Log.print("✅ Socket is already connected. No need to re-init.");
|
||||
return;
|
||||
}
|
||||
|
||||
String driverId = box.read(BoxName.driverID).toString();
|
||||
String token = box.read(BoxName.tokenDriver).toString();
|
||||
String platform = Platform.isIOS ? 'ios' : 'android';
|
||||
|
||||
_isInitializingSocket = true;
|
||||
|
||||
// تنظيف السوكيت القديم فقط إذا كان موجوداً وغير متصل
|
||||
if (socket != null) {
|
||||
Log.print("🧹 Cleaning up old socket instance...");
|
||||
socket!.clearListeners();
|
||||
socket!.dispose();
|
||||
socket = null;
|
||||
}
|
||||
|
||||
Log.print(
|
||||
"🟡 [LocationController] Initializing NEW Socket for Driver: $driverId");
|
||||
|
||||
try {
|
||||
// العودة للـ Websocket حصراً لأنه الوحيد الذي ينجح في فتح القناة
|
||||
socket = IO.io(
|
||||
'https://location.intaleq.xyz',
|
||||
IO.OptionBuilder()
|
||||
.setTransports(['websocket'])
|
||||
.setQuery({'driver_id': driverId, 'token': token, 'EIO': '3'})
|
||||
.enableForceNew()
|
||||
.build());
|
||||
|
||||
_setupSocketListeners();
|
||||
socket!.connect();
|
||||
} catch (e) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print("❌ Socket Initialization Exception: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void _setupSocketListeners() {
|
||||
if (socket == null) return;
|
||||
|
||||
socket!.off('connect');
|
||||
socket!.off('disconnect');
|
||||
socket!.off('connect_error');
|
||||
socket!.off('error');
|
||||
|
||||
socket!.onConnect((_) {
|
||||
_isInitializingSocket = false;
|
||||
|
||||
// ننتظر قليلاً للتأكد من تعبئة الـ IDs
|
||||
Future.delayed(const Duration(milliseconds: 1000), () {
|
||||
String? sid = socket?.id;
|
||||
String? eid = socket?.io.engine?.id;
|
||||
|
||||
Log.print(
|
||||
'✅ Socket Connected! ID: ${sid ?? eid ?? 'N/A'} (SID: $sid, EID: $eid)');
|
||||
|
||||
if (sid != null || eid != null) {
|
||||
isSocketConnected = true;
|
||||
_startHeartbeat();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket!.onDisconnect((data) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print('❌ Socket Disconnected: $data');
|
||||
isSocketConnected = false;
|
||||
_stopHeartbeat();
|
||||
});
|
||||
|
||||
socket!.onConnectError((err) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print('❌ Socket Connect Error: $err');
|
||||
});
|
||||
|
||||
socket!.onConnectTimeout((data) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print('❌ Socket Connect Timeout: $data');
|
||||
});
|
||||
|
||||
socket!.onError((err) {
|
||||
_isInitializingSocket = false;
|
||||
Log.print('❌ Socket General Error: $err');
|
||||
});
|
||||
|
||||
socket!.on('reconnect_attempt', (attempt) {
|
||||
Log.print('🔄 Socket Reconnecting... Attempt: $attempt');
|
||||
});
|
||||
|
||||
// 🔥 الاستماع للطلبات الجديدة
|
||||
socket!.on('new_ride_request', (data) {
|
||||
Log.print("🔔 Socket: New Ride Request Arrived!");
|
||||
|
||||
// نستخدم Future.microtask لضمان عدم حظر الـ UI Thread
|
||||
Future.microtask(() {
|
||||
if (data != null) {
|
||||
try {
|
||||
List<dynamic> rawList = [];
|
||||
if (data is String) {
|
||||
var decoded = jsonDecode(data);
|
||||
if (decoded is List) rawList = decoded;
|
||||
} else if (data is List) {
|
||||
if (data.isNotEmpty) {
|
||||
rawList = (data[0] is List) ? data[0] : data;
|
||||
}
|
||||
}
|
||||
|
||||
if (rawList.isNotEmpty) {
|
||||
Map<String, dynamic> convertedMap = {};
|
||||
for (int i = 0; i < rawList.length; i++) {
|
||||
convertedMap[i.toString()] = rawList[i];
|
||||
}
|
||||
handleIncomingOrder(convertedMap, "Socket");
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("❌ Error processing socket data: $e");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 🔥 الاستماع للإلغاء
|
||||
socket!.on('cancel_ride', (data) {
|
||||
Log.print("🚫 Socket: Ride Cancelled Event Received");
|
||||
String reason = data['reason'] ?? 'No reason provided';
|
||||
if (Get.isRegistered<MapDriverController>()) {
|
||||
Get.find<MapDriverController>()
|
||||
.processRideCancelledByPassenger(reason, source: "Socket");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// داخل LocationController
|
||||
|
||||
Future<void> handleIncomingOrder(
|
||||
Map<String, dynamic> rideData, String source) async {
|
||||
Log.print("📦 Socket Order Received from ($source)");
|
||||
|
||||
// 🔴 1. التحقق من حالة التطبيق قبل أي شيء 🔴
|
||||
bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false;
|
||||
|
||||
if (!isAppInForeground) {
|
||||
Log.print(
|
||||
"📱 [LocationController] Order received in background (iOS/Android). Source: $source");
|
||||
|
||||
if (Platform.isIOS) {
|
||||
// على iOS، نقوم بإظهار إشعار محلي لأن الـ Overlay غير مدعوم
|
||||
NotificationController().showNotification(
|
||||
"طلب رحلة جديد 🚖",
|
||||
"لديك طلب رحلة جديد، افتح التطبيق للموافقة عليه",
|
||||
jsonEncode(rideData),
|
||||
'ding.wav');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. التحقق من صحة البيانات
|
||||
if (rideData.isEmpty || !rideData.containsKey('16')) {
|
||||
Log.print("❌ Socket Error: Invalid Ride Data.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. تجهيز البيانات (DriverList)
|
||||
List<dynamic> driverList = [];
|
||||
if (rideData.isNotEmpty) {
|
||||
var sortedKeys = rideData.keys
|
||||
.where((e) => int.tryParse(e) != null)
|
||||
.map((e) => int.parse(e))
|
||||
.toList()..sort();
|
||||
|
||||
for (var key in sortedKeys) {
|
||||
driverList.add(rideData[key.toString()]);
|
||||
}
|
||||
}
|
||||
|
||||
// الحماية ضد البنية غير المكتملة
|
||||
if (driverList.length <= 16) {
|
||||
Log.print("❌ Socket Error: Parsed driver list is incomplete.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. إغلاق النافذة (إن وجدت بالخطأ) والتنقل
|
||||
try {
|
||||
if (await TripOverlayPlugin.isOverlayActive()) {
|
||||
Log.print("📲 Closing Overlay because App took control via Socket");
|
||||
await TripOverlayPlugin.hideOverlay();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Overlay check error: $e");
|
||||
}
|
||||
|
||||
// 🔥 [Fix Active-Ride Guard] منع فتح صفحة الطلبات أثناء وجود السائق في رحلة نشطة
|
||||
// هذا يمنع socket event جديد من تعطيل رحلة جارية
|
||||
String? currentRideStatus = box.read(BoxName.rideStatus);
|
||||
bool hasActiveRide = (currentRideStatus == 'Begin' ||
|
||||
currentRideStatus == 'Apply' ||
|
||||
currentRideStatus == 'Arrived');
|
||||
String currentRoute = Get.currentRoute;
|
||||
bool isOnMapPage = currentRoute.contains('MapPage') ||
|
||||
currentRoute.contains('PassengerLocation');
|
||||
|
||||
if (hasActiveRide || isOnMapPage) {
|
||||
Log.print(
|
||||
"⛔ [LocationController] Ignoring new ride request — driver has active ride ($currentRideStatus) or is on map page ($currentRoute).");
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentRoute != '/OrderRequestPage') {
|
||||
Log.print("🚀 Socket: Navigating to OrderRequestPage...");
|
||||
Get.toNamed('/OrderRequestPage', arguments: {
|
||||
'myListString': jsonEncode(driverList),
|
||||
'DriverList': driverList,
|
||||
'body': 'New Trip Request via Socket ⚡'
|
||||
});
|
||||
} else {
|
||||
Log.print(
|
||||
"⚠️ User is already on OrderRequestPage. Skipping navigation.");
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("❌ Socket Navigation Error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void _startHeartbeat() {
|
||||
_socketHeartbeat?.cancel();
|
||||
_socketHeartbeat = Timer.periodic(const Duration(seconds: 25), (timer) {
|
||||
// [Fix 6] تخطي الإرسال إذا كان stream الموقع نشطاً.
|
||||
// الـ _locSub يرسل update_location عند كل تحرك (كل 5-10 ثوانٍ) تلقائياً.
|
||||
// الـ heartbeat يكون مفيداً فقط عندما يتوقف الـ stream (الجهاز ثابت أو أوقف الخدمة).
|
||||
if (_locSub != null) return;
|
||||
if (socket != null && isSocketConnected && myLocation.latitude != 0) {
|
||||
emitLocationToSocket(myLocation, heading, speed);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _stopHeartbeat() {
|
||||
_socketHeartbeat?.cancel();
|
||||
}
|
||||
|
||||
// In LocationController.dart
|
||||
|
||||
void emitLocationToSocket(LatLng pos, double head, double spd) {
|
||||
String status = box.read(BoxName.statusDriverLocation) ?? 'on';
|
||||
String? currentRideStatus = box.read(BoxName.rideStatus);
|
||||
String? storedPassengerId = box.read(BoxName.passengerID);
|
||||
String? storedRideId = box.read(BoxName.rideId);
|
||||
|
||||
// Basic payload
|
||||
var payload = {
|
||||
'driver_id': box.read(BoxName.driverID),
|
||||
'lat': pos.latitude,
|
||||
'lng': pos.longitude,
|
||||
'heading': head,
|
||||
'speed': spd * 3.6,
|
||||
'status': status,
|
||||
'distance': totalDistance,
|
||||
};
|
||||
|
||||
// 🔥 القرار الذكي: حقن بيانات الراكب إذا كان هناك رحلة نشطة في الـ Box 🔥
|
||||
bool hasActiveRide = (currentRideStatus == 'Begin' ||
|
||||
currentRideStatus == 'Apply' ||
|
||||
currentRideStatus == 'Arrived');
|
||||
|
||||
if (hasActiveRide && storedPassengerId != null) {
|
||||
payload['passenger_id'] = storedPassengerId;
|
||||
payload['ride_id'] = storedRideId;
|
||||
}
|
||||
|
||||
// DebugLog.print to verify
|
||||
//Log.print('🚀 Emitting Location: $payload');
|
||||
|
||||
if (socket != null && socket!.connected) {
|
||||
socket!.emit('update_location', payload);
|
||||
}
|
||||
}
|
||||
// ===================================================================
|
||||
// ====== Tracking Logic ======
|
||||
// ===================================================================
|
||||
|
||||
Future<void> startLocationUpdates() async {
|
||||
_isReady = true;
|
||||
String currentStatus = box.read(BoxName.statusDriverLocation) ?? 'off';
|
||||
if (currentStatus == 'blocked') {
|
||||
stopLocationUpdates();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start background service
|
||||
await BackgroundServiceHelper.startService();
|
||||
|
||||
if (socket == null || !socket!.connected) {
|
||||
initSocket();
|
||||
}
|
||||
|
||||
if (_locSub != null) return;
|
||||
|
||||
if (await _ensureServiceAndPermission()) {
|
||||
_subscribeLocationStream();
|
||||
_startBatchTimers();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _subscribeLocationStream() async {
|
||||
_locSub?.cancel();
|
||||
int interval = _isPowerSavingMode ? 10000 : 5000;
|
||||
await location.enableBackgroundMode(enable: true);
|
||||
location.changeSettings(
|
||||
accuracy: LocationAccuracy.navigation,
|
||||
interval: interval,
|
||||
distanceFilter: _isPowerSavingMode ? 20 : 10,
|
||||
);
|
||||
|
||||
_locSub = location.onLocationChanged.listen((LocationData loc) async {
|
||||
if (loc.latitude == null || loc.longitude == null) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
final pos = LatLng(loc.latitude!, loc.longitude!);
|
||||
|
||||
myLocation = pos;
|
||||
speed = loc.speed ?? 0.0;
|
||||
heading = loc.heading ?? 0.0;
|
||||
|
||||
box.write('last_lat', pos.latitude);
|
||||
box.write('last_lng', pos.longitude);
|
||||
box.write('last_heading', heading);
|
||||
|
||||
if (_lastPosForDistance != null) {
|
||||
final d = _calculateDistance(_lastPosForDistance!, pos);
|
||||
if (d > 5.0) totalDistance += d;
|
||||
}
|
||||
_lastPosForDistance = pos;
|
||||
|
||||
update();
|
||||
emitLocationToSocket(pos, heading, speed);
|
||||
|
||||
if (Get.isRegistered<HomeCaptainController>()) {
|
||||
final homeCtrl = Get.find<HomeCaptainController>();
|
||||
if (homeCtrl.isActive &&
|
||||
homeCtrl.mapHomeCaptainController != null &&
|
||||
homeCtrl.isHomeMapActive &&
|
||||
homeCtrl.isMapReadyForCommands) {
|
||||
homeCtrl.mapHomeCaptainController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(pos, 17.5),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await _saveBehaviorIfMoved(pos, now, currentSpeed: speed);
|
||||
}, onError: (e) => Log.print('❌ Location Stream Error: $e'));
|
||||
}
|
||||
|
||||
Timer? _socketWatchdogTimer;
|
||||
|
||||
Future<void> stopLocationUpdates() async {
|
||||
Log.print("🛑 Stopping Location Updates...");
|
||||
|
||||
_locSub?.cancel();
|
||||
_locSub = null;
|
||||
_recordTimer?.cancel();
|
||||
_uploadBatchTimer?.cancel();
|
||||
_socketHeartbeat?.cancel();
|
||||
_socketWatchdogTimer?.cancel();
|
||||
|
||||
if (socket != null) {
|
||||
socket!.clearListeners();
|
||||
socket!.dispose();
|
||||
}
|
||||
|
||||
if (!Platform.isIOS) {
|
||||
await BackgroundServiceHelper.stopService();
|
||||
}
|
||||
|
||||
socket = null;
|
||||
isSocketConnected = false;
|
||||
_isReady = false;
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// ====== Batch Logic & Helpers ======
|
||||
// ===================================================================
|
||||
|
||||
void _startBatchTimers() {
|
||||
_recordTimer?.cancel();
|
||||
_uploadBatchTimer?.cancel();
|
||||
_socketWatchdogTimer?.cancel();
|
||||
|
||||
final recDur =
|
||||
_isPowerSavingMode ? recordIntervalPowerSave : recordIntervalNormal;
|
||||
final upDur = _isPowerSavingMode
|
||||
? uploadBatchIntervalPowerSave
|
||||
: uploadBatchIntervalNormal;
|
||||
|
||||
_recordTimer =
|
||||
Timer.periodic(recDur, (_) => _recordCurrentLocationToBuffer());
|
||||
_uploadBatchTimer = Timer.periodic(upDur, (_) => _flushBufferToServer());
|
||||
|
||||
// محاولة إعادة الاتصال بالسوكيت إذا انقطع كل 3 ثواني
|
||||
_socketWatchdogTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||||
if (!isSocketConnected && !_isInitializingSocket) {
|
||||
Log.print("🔄 Socket Watchdog: Attempting to reconnect socket...");
|
||||
initSocket();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _recordCurrentLocationToBuffer() {
|
||||
if (myLocation.latitude == 0) return;
|
||||
final now = DateTime.now();
|
||||
double distFromLast = 0.0;
|
||||
if (_lastRecordedRealLoc != null) {
|
||||
distFromLast = _calculateDistance(_lastRecordedRealLoc!, myLocation);
|
||||
}
|
||||
bool moved = distFromLast > 10.0;
|
||||
bool timeForced = _lastRecordedTime == null ||
|
||||
now.difference(_lastRecordedTime!).inSeconds >= 60;
|
||||
|
||||
if ((moved && speed > 0.5) || timeForced) {
|
||||
_lastRecordedRealLoc = myLocation;
|
||||
_lastRecordedTime = now;
|
||||
final point = {
|
||||
'lat': double.parse(myLocation.latitude.toStringAsFixed(6)),
|
||||
'lng': double.parse(myLocation.longitude.toStringAsFixed(6)),
|
||||
'spd': double.parse((speed * 3.6).toStringAsFixed(1)),
|
||||
'head': int.parse(heading.toStringAsFixed(0)),
|
||||
'st': box.read(BoxName.statusDriverLocation) ?? 'off',
|
||||
'ts': now.toIso8601String(),
|
||||
};
|
||||
_trackBuffer.add(point);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _flushBufferToServer() async {
|
||||
if (_trackBuffer.isEmpty) return;
|
||||
|
||||
int itemsToTake = _trackBuffer.length > 100 ? 100 : _trackBuffer.length;
|
||||
List<Map<String, dynamic>> batch = _trackBuffer.sublist(0, itemsToTake);
|
||||
|
||||
final String driverId = (box.read(BoxName.driverID) ?? '').toString();
|
||||
try {
|
||||
var res = await CRUD().post(
|
||||
link: '${AppLink.locationServer}/add_batch.php',
|
||||
payload: {'driver_id': driverId, 'batch_data': jsonEncode(batch)},
|
||||
);
|
||||
if (res != 'failure') {
|
||||
_trackBuffer.removeRange(0, itemsToTake);
|
||||
} else {
|
||||
_enforceBufferLimit();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('❌ Failed to upload batch: $e');
|
||||
_enforceBufferLimit();
|
||||
}
|
||||
}
|
||||
|
||||
void _enforceBufferLimit() {
|
||||
if (_trackBuffer.length > 500) {
|
||||
_trackBuffer.removeRange(0, _trackBuffer.length - 500);
|
||||
Log.print("⚠️ Buffer limit enforced. Removed oldest entries.");
|
||||
}
|
||||
}
|
||||
|
||||
void _listenToBatteryChanges() async {
|
||||
_battery.onBatteryStateChanged.listen((state) async {
|
||||
int level = await _battery.batteryLevel;
|
||||
bool previousMode = _isPowerSavingMode;
|
||||
if (level <= powerSaveTriggerLevel) _isPowerSavingMode = true;
|
||||
if (level >= powerSaveExitLevel) _isPowerSavingMode = false;
|
||||
if (previousMode != _isPowerSavingMode) {
|
||||
_startBatchTimers();
|
||||
_updateLocationSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _updateLocationSettings() async {
|
||||
if (_locSub == null) return;
|
||||
int interval = _isPowerSavingMode ? 10000 : 5000;
|
||||
try {
|
||||
await location.changeSettings(
|
||||
accuracy: LocationAccuracy.navigation,
|
||||
interval: interval,
|
||||
distanceFilter: _isPowerSavingMode ? 20 : 10,
|
||||
);
|
||||
Log.print("🔋 Location settings updated. Power Save: $_isPowerSavingMode");
|
||||
} catch (e) {
|
||||
Log.print("❌ Failed to update location settings: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveBehaviorIfMoved(LatLng pos, DateTime now,
|
||||
{required double currentSpeed}) async {
|
||||
final dist =
|
||||
(_lastSqlLoc == null) ? 999.0 : _calculateDistance(_lastSqlLoc!, pos);
|
||||
if (dist < 15.0) return;
|
||||
|
||||
final accel = _calcAcceleration(currentSpeed, now) ?? 0.0;
|
||||
_lastSqlLoc = pos;
|
||||
|
||||
_behaviorBuffer.add({
|
||||
'driver_id': (box.read(BoxName.driverID) ?? '').toString(),
|
||||
'latitude': pos.latitude,
|
||||
'longitude': pos.longitude,
|
||||
'acceleration': accel,
|
||||
'created_at': now.toIso8601String(),
|
||||
'updated_at': now.toIso8601String(),
|
||||
});
|
||||
|
||||
if (_behaviorBuffer.length >= 10) {
|
||||
_flushBehaviorBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
void _flushBehaviorBuffer() {
|
||||
if (_behaviorBuffer.isEmpty) return;
|
||||
List<Map<String, dynamic>> batch = List.from(_behaviorBuffer);
|
||||
_behaviorBuffer.clear();
|
||||
|
||||
Future.microtask(() async {
|
||||
try {
|
||||
for (var data in batch) {
|
||||
await sql.insertData(data, TableName.behavior);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('SQLite Batch Insert Error: $e');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// استبدال دالة Haversine اليدوية بـ Geolocator في باقي الكود أيضاً
|
||||
// لأنها تعتمد على C++ في الأندرويد و Obj-C في الآيفون (Native Speed)
|
||||
double _calculateDistance(LatLng a, LatLng b) {
|
||||
return geo.Geolocator.distanceBetween(
|
||||
a.latitude, a.longitude, b.latitude, b.longitude);
|
||||
}
|
||||
|
||||
double? _calcAcceleration(double currentSpeed, DateTime now) {
|
||||
if (_lastSpeed != null && _lastSpeedAt != null) {
|
||||
final dt = now.difference(_lastSpeedAt!).inMilliseconds / 1000.0;
|
||||
if (dt > 0.5) {
|
||||
final a = (currentSpeed - _lastSpeed!) / dt;
|
||||
_lastSpeed = currentSpeed;
|
||||
_lastSpeedAt = now;
|
||||
return a;
|
||||
}
|
||||
}
|
||||
_lastSpeed = currentSpeed;
|
||||
_lastSpeedAt = now;
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _initLocationSettings() async {
|
||||
if (await _ensureServiceAndPermission()) {
|
||||
try {
|
||||
await location.enableBackgroundMode(enable: true);
|
||||
location.changeSettings(
|
||||
accuracy: LocationAccuracy.navigation,
|
||||
interval: 1000,
|
||||
distanceFilter: 10);
|
||||
} catch (e) {
|
||||
Log.print("Warning: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥🔥 هذه هي الدالة المعدلة التي تستخدم ph.Permission 🔥🔥
|
||||
Future<bool> _ensureServiceAndPermission() async {
|
||||
// 1. طلب إذن الإشعارات أولاً باستخدام permission_handler
|
||||
if (Platform.isAndroid) {
|
||||
var notificationStatus = await ph.Permission.notification.status;
|
||||
if (!notificationStatus.isGranted) {
|
||||
await ph.Permission.notification.request();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. طلب تفعيل خدمة الموقع (GPS) من بكج location
|
||||
bool serviceEnabled = await location.serviceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
serviceEnabled = await location.requestService();
|
||||
if (!serviceEnabled) return false;
|
||||
}
|
||||
|
||||
// 3. طلب إذن الموقع الأساسي من بكج location
|
||||
PermissionStatus permissionGranted = await location.hasPermission();
|
||||
if (permissionGranted == PermissionStatus.denied) {
|
||||
permissionGranted = await location.requestPermission();
|
||||
if (permissionGranted != PermissionStatus.granted) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
// ... (باقي الكود)
|
||||
|
||||
Future<LocationData?> getLocation() async {
|
||||
try {
|
||||
if (await _ensureServiceAndPermission()) {
|
||||
final locData = await location.getLocation();
|
||||
if (locData != null && locData.latitude != null && locData.longitude != null) {
|
||||
myLocation = LatLng(locData.latitude!, locData.longitude!);
|
||||
heading = locData.heading ?? 0.0;
|
||||
speed = locData.speed ?? 0.0;
|
||||
|
||||
box.write('last_lat', myLocation.latitude);
|
||||
box.write('last_lng', myLocation.longitude);
|
||||
box.write('last_heading', heading);
|
||||
|
||||
update();
|
||||
|
||||
if (Get.isRegistered<HomeCaptainController>()) {
|
||||
final homeCtrl = Get.find<HomeCaptainController>();
|
||||
if (homeCtrl.mapHomeCaptainController != null &&
|
||||
homeCtrl.isMapReadyForCommands) {
|
||||
Log.print("📍 [LocationController] Animating camera to single location update");
|
||||
homeCtrl.mapHomeCaptainController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(myLocation, 17.5),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return locData;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('❌ FAILED to get single location: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
60
siro_driver/lib/controller/functions/location_permission.dart
Executable file
60
siro_driver/lib/controller/functions/location_permission.dart
Executable file
@@ -0,0 +1,60 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../../views/widgets/mydialoug.dart';
|
||||
import '../auth/captin/login_captin_controller.dart';
|
||||
|
||||
class LocationPermissions {
|
||||
// late Location location;
|
||||
|
||||
// Future locationPermissions() async {
|
||||
// location = Location();
|
||||
// var permissionStatus = await location.requestPermission();
|
||||
// if (permissionStatus == PermissionStatus.denied) {
|
||||
// // The user denied the location permission.
|
||||
// Get.defaultDialog(title: 'GPS Required Allow !.'.tr, middleText: '');
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
Future<void> getPermissionLocation() async {
|
||||
final PermissionStatus status = await Permission.locationAlways.status;
|
||||
if (!await Permission.locationAlways.serviceStatus.isEnabled) {
|
||||
Log.print('status.isGranted: ${status.isGranted}');
|
||||
// box.write(BoxName.locationPermission, 'true');
|
||||
await Permission.locationAlways.request();
|
||||
Get.put(LoginDriverController()).update();
|
||||
MyDialog().getDialog(
|
||||
'Enable Location Permission'.tr, // {en:ar}
|
||||
'Allowing location access will help us display orders near you. Please enable it now.'
|
||||
.tr, // {en:ar}
|
||||
() async {
|
||||
Get.back();
|
||||
box.write(BoxName.locationPermission, 'true');
|
||||
await Permission.locationAlways.request();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getPermissionLocation1() async {
|
||||
PermissionStatus status = await Permission.locationWhenInUse.request();
|
||||
|
||||
if (status.isGranted) {
|
||||
// After granting when in use, request "always" location permission
|
||||
status = await Permission.locationAlways.request();
|
||||
|
||||
if (status.isGranted) {
|
||||
print("Background location permission granted");
|
||||
} else {
|
||||
print("Background location permission denied");
|
||||
}
|
||||
} else {
|
||||
print("Location permission denied");
|
||||
await openAppSettings();
|
||||
}
|
||||
}
|
||||
157
siro_driver/lib/controller/functions/log_out.dart
Executable file
157
siro_driver/lib/controller/functions/log_out.dart
Executable file
@@ -0,0 +1,157 @@
|
||||
import 'package:siro_driver/views/home/on_boarding_page.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/my_textField.dart';
|
||||
|
||||
import '../../constant/style.dart';
|
||||
|
||||
class LogOutController extends GetxController {
|
||||
TextEditingController checkTxtController = TextEditingController();
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final formKey1 = GlobalKey<FormState>();
|
||||
final emailTextController = TextEditingController();
|
||||
|
||||
Future deleteMyAccountDriver(String id) async {
|
||||
await CRUD().post(link: AppLink.removeUser, payload: {'id': id}).then(
|
||||
(value) => Get.snackbar('Deleted'.tr, 'Your Account is Deleted',
|
||||
backgroundColor: AppColor.redColor));
|
||||
}
|
||||
|
||||
checkBeforeDelete() async {
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.deletecaptainAccounr,
|
||||
payload: {'id': box.read(BoxName.driverID)});
|
||||
return res['message'][0]['id'];
|
||||
}
|
||||
|
||||
deletecaptainAccount() {
|
||||
Get.defaultDialog(
|
||||
backgroundColor: AppColor.yellowColor,
|
||||
title: 'Are you sure to delete your account?'.tr,
|
||||
middleText:
|
||||
'Your data will be erased after 2 weeks\nAnd you will can\'t return to use app after 1 month ',
|
||||
titleStyle: AppStyle.title,
|
||||
content: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: Get.width,
|
||||
decoration: AppStyle.boxDecoration,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Your data will be erased after 2 weeks\nAnd you will can\'t return to use app after 1 month'
|
||||
.tr,
|
||||
style: AppStyle.title.copyWith(color: AppColor.redColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Form(
|
||||
key: formKey,
|
||||
child: SizedBox(
|
||||
width: Get.width,
|
||||
child: MyTextForm(
|
||||
controller: checkTxtController,
|
||||
label: 'Enter Your First Name'.tr,
|
||||
hint: 'Enter Your First Name'.tr,
|
||||
type: TextInputType.name,
|
||||
),
|
||||
))
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Delete'.tr,
|
||||
onPressed: () async {
|
||||
if (checkTxtController.text == (box.read(BoxName.nameDriver))) {
|
||||
// deletecaptainAccount();
|
||||
|
||||
var id = await checkBeforeDelete();
|
||||
deleteMyAccountDriver(id);
|
||||
} else {
|
||||
mySnackeBarError('Your Name is Wrong'.tr);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Future logOutPassenger() async {
|
||||
Get.defaultDialog(
|
||||
title: 'Are you Sure to LogOut?'.tr,
|
||||
content: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
title: 'Cancel'.tr,
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(AppColor.redColor),
|
||||
),
|
||||
onPressed: () async {
|
||||
// box.remove(BoxName.agreeTerms);
|
||||
await box.erase();
|
||||
await storage.deleteAll();
|
||||
Get.offAll(OnBoardingPage());
|
||||
},
|
||||
child: Text(
|
||||
'Sign Out'.tr,
|
||||
style:
|
||||
AppStyle.title.copyWith(color: AppColor.secondaryColor),
|
||||
))
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
Future logOutCaptain() async {
|
||||
Get.defaultDialog(
|
||||
title: 'Are you Sure to LogOut?'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
title: 'Cancel'.tr,
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(AppColor.redColor),
|
||||
),
|
||||
onPressed: () async {
|
||||
// box.remove(BoxName.agreeTerms);
|
||||
await box.erase();
|
||||
await storage.deleteAll();
|
||||
Get.offAll(OnBoardingPage());
|
||||
},
|
||||
child: Text(
|
||||
'Sign Out'.tr,
|
||||
style:
|
||||
AppStyle.title.copyWith(color: AppColor.secondaryColor),
|
||||
))
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
deletePassengerAccount() async {
|
||||
if (formKey1.currentState!.validate()) {
|
||||
if (box.read(BoxName.email).toString() == emailTextController.text) {
|
||||
await CRUD().post(link: AppLink.passengerRemovedAccountEmail, payload: {
|
||||
'email': box.read(BoxName.email),
|
||||
});
|
||||
} else {
|
||||
mySnackeBarError(
|
||||
'Email you inserted is Wrong.'.tr,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'net_guard.dart';
|
||||
|
||||
typedef BodyEncoder = Future<http.Response> Function();
|
||||
|
||||
class HttpRetry {
|
||||
/// ريتراي لـ network/transient errors فقط.
|
||||
static Future<http.Response> sendWithRetry(
|
||||
BodyEncoder send, {
|
||||
int maxRetries = 3,
|
||||
Duration baseDelay = const Duration(milliseconds: 400),
|
||||
Duration timeout = const Duration(seconds: 12),
|
||||
}) async {
|
||||
// ✅ Pre-flight check for internet connection
|
||||
if (!await NetGuard().hasInternet()) {
|
||||
// Immediately throw a specific exception if there's no internet.
|
||||
// This avoids pointless retries.
|
||||
throw const SocketException("No internet connection");
|
||||
}
|
||||
int attempt = 0;
|
||||
while (true) {
|
||||
attempt++;
|
||||
try {
|
||||
final res = await send().timeout(timeout);
|
||||
return res;
|
||||
} on TimeoutException catch (_) {
|
||||
if (attempt >= maxRetries) rethrow;
|
||||
} on SocketException catch (_) {
|
||||
if (attempt >= maxRetries) rethrow;
|
||||
} on HandshakeException catch (_) {
|
||||
if (attempt >= maxRetries) rethrow;
|
||||
} on http.ClientException catch (e) {
|
||||
// مثال: Connection reset by peer
|
||||
final msg = e.message.toLowerCase();
|
||||
final transient = msg.contains('connection reset') ||
|
||||
msg.contains('broken pipe') ||
|
||||
msg.contains('timed out');
|
||||
if (!transient || attempt >= maxRetries) rethrow;
|
||||
}
|
||||
// backoff: 0.4s, 0.8s, 1.6s
|
||||
final delay = baseDelay * (1 << (attempt - 1));
|
||||
await Future.delayed(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
siro_driver/lib/controller/functions/network/net_guard.dart
Normal file
48
siro_driver/lib/controller/functions/network/net_guard.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:internet_connection_checker/internet_connection_checker.dart';
|
||||
|
||||
class NetGuard {
|
||||
static final NetGuard _i = NetGuard._();
|
||||
NetGuard._();
|
||||
factory NetGuard() => _i;
|
||||
|
||||
bool _notified = false;
|
||||
|
||||
/// فحص: (أ) فيه شبكة؟ (ب) فيه انترنت؟ (ج) السيرفر نفسه reachable؟
|
||||
Future<bool> hasInternet({Uri? mustReach}) async {
|
||||
final connectivity = await Connectivity().checkConnectivity();
|
||||
if (connectivity == ConnectivityResult.none) return false;
|
||||
|
||||
final hasNet =
|
||||
await InternetConnectionChecker.createInstance().hasConnection;
|
||||
if (!hasNet) return false;
|
||||
|
||||
if (mustReach != null) {
|
||||
try {
|
||||
final host = mustReach.host;
|
||||
final result = await InternetAddress.lookup(host);
|
||||
if (result.isEmpty || result.first.rawAddress.isEmpty) return false;
|
||||
|
||||
// اختباري خفيف عبر TCP (80/443) — 400ms timeout
|
||||
final port = mustReach.scheme == 'http' ? 80 : 443;
|
||||
final socket = await Socket.connect(host, port,
|
||||
timeout: const Duration(seconds: 1));
|
||||
socket.destroy();
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// إظهار إشعار مرة واحدة ثم إسكات التكرارات
|
||||
void notifyOnce(void Function(String title, String msg) show) {
|
||||
if (_notified) return;
|
||||
_notified = true;
|
||||
show('لا يوجد اتصال بالإنترنت', 'تحقق من الشبكة ثم حاول مجددًا.');
|
||||
// إعادة السماح بعد 15 ثانية
|
||||
Future.delayed(const Duration(seconds: 15), () => _notified = false);
|
||||
}
|
||||
}
|
||||
649
siro_driver/lib/controller/functions/ocr_controller.dart
Executable file
649
siro_driver/lib/controller/functions/ocr_controller.dart
Executable file
@@ -0,0 +1,649 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/info.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/constant/table_names.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../auth/captin/register_captin_controller.dart';
|
||||
import 'launch.dart';
|
||||
|
||||
//
|
||||
// class TextExtractionController extends GetxController {
|
||||
// String extractedText = '';
|
||||
// bool isloading = false;
|
||||
// File? _scannedImage;
|
||||
// // Convert the extracted text to JSON
|
||||
// // Convert the extracted text to JSON
|
||||
// String getTextAsJSON(String text) {
|
||||
// final lines = text.split('\n');
|
||||
// final jsonList = lines.map((line) {
|
||||
// return {
|
||||
// 'line_text': line,
|
||||
// 'num_words': line.trim().split(' ').length,
|
||||
// };
|
||||
// }).toList();
|
||||
//
|
||||
// final json = {
|
||||
// 'lines': jsonList,
|
||||
// 'num_lines': lines.length,
|
||||
// };
|
||||
//
|
||||
// return jsonEncode(json);
|
||||
// }
|
||||
//
|
||||
// // Convert the extracted text to blocks by line
|
||||
// List<String> getTextBlocks(String text) {
|
||||
// return text.split('\n');
|
||||
// }
|
||||
//
|
||||
// // Future<void> pickAndExtractText() async {
|
||||
// // final pickedImage = await ImagePicker().pickImage(
|
||||
// // source: ImageSource.camera,
|
||||
// // preferredCameraDevice: CameraDevice.rear,
|
||||
// // maxHeight: Get.height * .3,
|
||||
// // maxWidth: Get.width * .8,
|
||||
// // imageQuality: 99,
|
||||
// // );
|
||||
// // if (pickedImage != null) {
|
||||
// // isloading = true;
|
||||
// // update();
|
||||
// // final imagePath = pickedImage.path;
|
||||
// // final languages = [
|
||||
// // 'eng',
|
||||
// // 'ara'
|
||||
// // ]; // Specify the languages you want to use for text extraction
|
||||
//
|
||||
// // try {
|
||||
// // final text = await FlutterTesseractOcr.extractText(imagePath,
|
||||
// // language:
|
||||
// // languages.join('+'), // Combine multiple languages with '+'
|
||||
// // args: {
|
||||
// // "psm": "4",
|
||||
// // "preserve_interword_spaces": "1",
|
||||
// // // "rectangle": const Rect.fromLTWH(100, 100, 200, 200),
|
||||
// // } // Additional options if needed
|
||||
// // );
|
||||
// // isloading = false;
|
||||
// // final jsonText = getTextAsJSON(text);
|
||||
// // final textBlocks = getTextBlocks(text);
|
||||
// // update();
|
||||
// // extractedText =
|
||||
// // textBlocks.toString(); // Convert the extracted text to JSON.
|
||||
//
|
||||
// // // Print the JSON to the console.
|
||||
// // update();
|
||||
// // } catch (e) {
|
||||
// // extractedText = '';
|
||||
// // }
|
||||
// // }
|
||||
// // }
|
||||
// }
|
||||
|
||||
// class TextMLGoogleRecognizerController extends GetxController {
|
||||
// @override
|
||||
// void onInit() {
|
||||
// scanText();
|
||||
// super.onInit();
|
||||
// }
|
||||
//
|
||||
// // The ImagePicker instance
|
||||
// final ImagePicker _imagePicker = ImagePicker();
|
||||
//
|
||||
// // The GoogleMlKit TextRecognizer instance
|
||||
// final TextRecognizer _textRecognizer = TextRecognizer();
|
||||
//
|
||||
// // The scanned text
|
||||
// String? scannedText;
|
||||
// String? jsonOutput;
|
||||
// final List<Map<String, dynamic>> lines = [];
|
||||
//
|
||||
// Map decode = {};
|
||||
//
|
||||
// Future<void> scanText() async {
|
||||
// // Pick an image from the camera or gallery
|
||||
// final XFile? image =
|
||||
// await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
//
|
||||
// // If no image was picked, return
|
||||
// if (image == null) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Convert the XFile object to an InputImage object
|
||||
// final InputImage inputImage = InputImage.fromFile(File(image.path));
|
||||
//
|
||||
// // Recognize the text in the image
|
||||
// final RecognizedText recognizedText =
|
||||
// await _textRecognizer.processImage(inputImage);
|
||||
// scannedText = recognizedText.text;
|
||||
// Map extractedData = {};
|
||||
// // Extract the scanned text line by line
|
||||
// for (var i = 0; i < recognizedText.blocks.length; i++) {
|
||||
// final block = recognizedText.blocks[i];
|
||||
// for (final line in block.lines) {
|
||||
// final lineText = line.text;
|
||||
//
|
||||
// if (lineText.contains('DL')) {
|
||||
// final dlNumber = lineText.split('DL')[1].trim();
|
||||
// extractedData['dl_number'] = dlNumber;
|
||||
// }
|
||||
// if (lineText.contains('USA')) {
|
||||
// final usa = lineText.split('USA')[1].trim();
|
||||
// extractedData['USA'] = usa;
|
||||
// }
|
||||
// if (lineText.contains('DRIVER LICENSE')) {
|
||||
// final driverl = lineText;
|
||||
// extractedData['DRIVER_LICENSE'] = driverl;
|
||||
// }
|
||||
//
|
||||
// if (lineText.contains('EXP')) {
|
||||
// final expiryDate = lineText.split('EXP')[1].trim();
|
||||
// extractedData['expiry_date'] = expiryDate;
|
||||
// }
|
||||
//
|
||||
// if (lineText.contains('DOB')) {
|
||||
// final dob = lineText.split('DOB')[1].trim();
|
||||
// extractedData['dob'] = dob;
|
||||
// }
|
||||
//
|
||||
// if (lineText.contains("LN")) {
|
||||
// if ((lineText.indexOf("LN") == 0)) {
|
||||
// final lastName = lineText.split('LN')[1].trim();
|
||||
// extractedData['lastName'] = lastName;
|
||||
// }
|
||||
// }
|
||||
// if (lineText.contains("FN")) {
|
||||
// final firstName = lineText.split('FN')[1].trim();
|
||||
// extractedData['firstName'] = firstName;
|
||||
// }
|
||||
// if (lineText.contains("RSTR")) {
|
||||
// final rstr = lineText.split('RSTR')[1].trim();
|
||||
// extractedData['rstr'] = rstr;
|
||||
// }
|
||||
// if (lineText.contains("CLASS")) {
|
||||
// final class1 = lineText.split('CLASS')[1].trim();
|
||||
// extractedData['class'] = class1;
|
||||
// }
|
||||
// if (lineText.contains("END")) {
|
||||
// final end = lineText.split('END')[1].trim();
|
||||
// extractedData['end'] = end;
|
||||
// }
|
||||
// if (lineText.contains("DD")) {
|
||||
// final dd = lineText.split('DD')[1].trim();
|
||||
// extractedData['dd'] = dd;
|
||||
// }
|
||||
// if (lineText.contains("EYES")) {
|
||||
// final eyes = lineText.split('EYES')[1].trim();
|
||||
// extractedData['eyes'] = eyes;
|
||||
// }
|
||||
// if (lineText.contains("SEX")) {
|
||||
// final parts = lineText.split("SEX ")[1];
|
||||
// extractedData['sex'] = parts[0];
|
||||
// }
|
||||
// if (lineText.contains("HAIR")) {
|
||||
// final hair = lineText.split('HAIR')[1].trim();
|
||||
// extractedData['hair'] = hair;
|
||||
// }
|
||||
//
|
||||
// if (lineText.contains('STREET') || lineText.contains(',')) {
|
||||
// final address = lineText;
|
||||
// extractedData['address'] = address;
|
||||
// }
|
||||
//
|
||||
// // Repeat this process for other relevant data fields
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Convert the list of lines to a JSON string
|
||||
// jsonOutput = jsonEncode(extractedData);
|
||||
// decode = jsonDecode(jsonOutput!);
|
||||
//
|
||||
// update();
|
||||
// }
|
||||
// }
|
||||
|
||||
class ScanDocumentsByApi extends GetxController {
|
||||
bool isLoading = false;
|
||||
Map<String, dynamic> responseMap = {};
|
||||
final ImagePicker imagePicker = ImagePicker();
|
||||
late Uint8List imagePortrait;
|
||||
late Uint8List imageSignature;
|
||||
late Uint8List imageDocumentFrontSide;
|
||||
XFile? image;
|
||||
XFile? imagePortraitFile;
|
||||
XFile? imageFace;
|
||||
late File tempFile;
|
||||
late String imagePath;
|
||||
DateTime now = DateTime.now();
|
||||
late String name;
|
||||
late String licenseClass;
|
||||
late String documentNo;
|
||||
late String address;
|
||||
late String stateCode;
|
||||
late String height;
|
||||
late String sex;
|
||||
late String postalCode;
|
||||
late String dob;
|
||||
late String expireDate;
|
||||
|
||||
// ///////////////////////
|
||||
// late CameraController cameraController;
|
||||
// late List<CameraDescription> cameras;
|
||||
// bool isCameraInitialized = false;
|
||||
// // final TextRecognizer _textRecognizer = TextRecognizer();
|
||||
// String? scannedText;
|
||||
|
||||
// Future<void> initializeCamera(int cameraID) async {
|
||||
// try {
|
||||
// cameras = await availableCameras();
|
||||
// //update();
|
||||
// cameraController = CameraController(
|
||||
// cameras[cameraID],
|
||||
// ResolutionPreset.medium,
|
||||
// enableAudio: false,
|
||||
// );
|
||||
// await cameraController.initialize();
|
||||
// isCameraInitialized = true;
|
||||
// update();
|
||||
// } catch (e) {
|
||||
// if (e is CameraException) {
|
||||
// switch (e.code) {
|
||||
// case 'CameraAccessDenied':
|
||||
// Get.defaultDialog(
|
||||
// title: 'Camera Access Denied.'.tr,
|
||||
// middleText: '',
|
||||
// confirm:
|
||||
// MyElevatedButton(title: 'Open Settings', onPressed: () {}),
|
||||
// );
|
||||
// break;
|
||||
// default:
|
||||
// // Handle other errors here.
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
///
|
||||
|
||||
Future<void> scanDocumentsByApi() async {
|
||||
// String? visionApi = await storage.read(key: BoxName.visionApi);
|
||||
// String? visionApi = AK.visionApi;
|
||||
// Pick an image from the camera or gallery
|
||||
image = await imagePicker.pickImage(source: ImageSource.camera); //
|
||||
|
||||
// If no image was picked, return
|
||||
if (image == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
update();
|
||||
var headers = {'X-BLOBR-KEY': AK.visionApi};
|
||||
var request = http.MultipartRequest('POST',
|
||||
Uri.parse('https://api.faceonlive.com/j2y3q25y1b6maif1/api/iddoc'));
|
||||
request.files.add(await http.MultipartFile.fromPath('image', image!.path));
|
||||
request.headers.addAll(headers);
|
||||
|
||||
http.StreamedResponse response = await request.send();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
String responseString = await response.stream.bytesToString();
|
||||
responseMap = jsonDecode(responseString);
|
||||
var ocrData = responseMap['data']['ocr'];
|
||||
name = ocrData['name'].toString();
|
||||
licenseClass = ocrData['dlClass'].toString();
|
||||
documentNo = ocrData['documentNumber'].toString();
|
||||
address = ocrData['address'].toString();
|
||||
height = ocrData['height'].toString();
|
||||
postalCode = ocrData['addressPostalCode'].toString();
|
||||
sex = ocrData['sex'].toString();
|
||||
stateCode = ocrData['addressJurisdictionCode'].toString();
|
||||
expireDate = ocrData['dateOfExpiry'].toString();
|
||||
dob = ocrData['dateOfBirth'].toString();
|
||||
if (responseMap['data'] != null &&
|
||||
responseMap['data']['image'] != null &&
|
||||
responseMap['data']['image']['portrait'] != null) {
|
||||
imagePortrait = base64Decode(responseMap['data']['image']['portrait']);
|
||||
String tempPath = Directory.systemTemp.path;
|
||||
tempFile = File('$tempPath/image.jpg');
|
||||
await tempFile.writeAsBytes(imagePortrait);
|
||||
|
||||
imagePath = tempFile.path;
|
||||
// imagePortraitFile=File(imagePath) ;
|
||||
update();
|
||||
} else {
|
||||
// Handle error or provide a default value
|
||||
}
|
||||
|
||||
if (responseMap['data']['image']['signature'] != null) {
|
||||
imageSignature =
|
||||
base64Decode(responseMap['data']['image']['signature']);
|
||||
} else {
|
||||
imageSignature = imagePortrait;
|
||||
// Handle error or provide a default value
|
||||
}
|
||||
|
||||
if (responseMap['data'] != null &&
|
||||
responseMap['data']['image'] != null &&
|
||||
responseMap['data']['image']['documentFrontSide'] != null) {
|
||||
imageDocumentFrontSide =
|
||||
base64Decode(responseMap['data']['image']['documentFrontSide']);
|
||||
} else {
|
||||
// Handle error or provide a default value
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
} else {}
|
||||
}
|
||||
|
||||
late int times;
|
||||
Future checkMatchFaceApi() async {
|
||||
sql.getAllData(TableName.faceDetectTimes).then((value) {
|
||||
if (value.isEmpty || value == null) {
|
||||
sql.insertData({'faceDetectTimes': 1}, TableName.faceDetectTimes);
|
||||
sql.getAllData(TableName.faceDetectTimes).then((value) {
|
||||
times = value[0]['faceDetectTimes'];
|
||||
update();
|
||||
});
|
||||
} else {
|
||||
if (times < 4) {
|
||||
times++;
|
||||
matchFaceApi();
|
||||
sql.updateData(
|
||||
{'faceDetectTimes': times}, TableName.faceDetectTimes, 1);
|
||||
} else {
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'You have finished all times '.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
middleText: 'if you want help you can email us here'.tr,
|
||||
middleTextStyle: AppStyle.title,
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Thanks'.tr,
|
||||
kolor: AppColor.greenColor,
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Email Us'.tr,
|
||||
kolor: AppColor.yellowColor, //
|
||||
onPressed: () {
|
||||
launchCommunication('email', 'support@mobile-app.store',
|
||||
'${'Hi'.tr} ${AppInformation.appName}\n${'I cant register in your app in face detection '.tr}');
|
||||
Get.back();
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Map res = {};
|
||||
Future matchFaceApi() async {
|
||||
// String? visionApi = await storage.read(key: BoxName.visionApi);
|
||||
imageFace = await imagePicker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
preferredCameraDevice: CameraDevice.front,
|
||||
);
|
||||
|
||||
// If no image was picked, return
|
||||
if (image == null) {
|
||||
return;
|
||||
}
|
||||
final imageFile = File(imageFace!.path);
|
||||
// Uint8List imageBytes = await imageFile.readAsBytes();
|
||||
var headers = {'X-BLOBR-KEY': AK.visionApi};
|
||||
var request = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse(
|
||||
'https://api.faceonlive.com/sntzbspfsdupgid1/api/face_compare'));
|
||||
request.files
|
||||
.add(await http.MultipartFile.fromPath('image1', imageFile.path));
|
||||
request.files.add(await http.MultipartFile.fromPath('image2', imagePath));
|
||||
request.headers.addAll(headers);
|
||||
|
||||
http.StreamedResponse response = await request.send();
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
res = jsonDecode(await response.stream.bytesToString());
|
||||
|
||||
update();
|
||||
res['data']['result'].toString().contains('No face detected in image')
|
||||
? Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'No face detected'.tr,
|
||||
middleText: ''.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
confirm: MyElevatedButton(
|
||||
kolor: AppColor.yellowColor,
|
||||
title: 'Back'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
)) //
|
||||
: Get.defaultDialog(
|
||||
// barrierDismissible: false,
|
||||
title: 'Image detecting result is '.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Column(
|
||||
children: [
|
||||
Text(
|
||||
res['data']['result'].toString(),
|
||||
style: res['data']['result'].toString() == 'Different'
|
||||
? AppStyle.title.copyWith(color: AppColor.redColor)
|
||||
: AppStyle.title.copyWith(color: AppColor.greenColor),
|
||||
),
|
||||
res['data']['result'].toString() == 'Different'
|
||||
? Text(
|
||||
'${'Be sure for take accurate images please\nYou have'.tr} $times ${'from 3 times Take Attention'.tr}',
|
||||
style: AppStyle.title,
|
||||
)
|
||||
: Text(
|
||||
'image verified'.tr,
|
||||
style: AppStyle.title,
|
||||
)
|
||||
],
|
||||
),
|
||||
confirm: res['data']['result'].toString() == 'Different'
|
||||
? MyElevatedButton(
|
||||
title: 'Back'.tr,
|
||||
onPressed: () => Get.back(),
|
||||
kolor: AppColor.redColor,
|
||||
)
|
||||
: MyElevatedButton(
|
||||
title: 'Next'.tr,
|
||||
onPressed: () async {
|
||||
RegisterCaptainController registerCaptainController =
|
||||
Get.put(RegisterCaptainController());
|
||||
|
||||
await registerCaptainController.register();
|
||||
await registerCaptainController.addLisence();
|
||||
await uploadImagePortrate();
|
||||
// Get.to(() => CarLicensePage());
|
||||
},
|
||||
// {
|
||||
// await uploadImage(
|
||||
// tempFile, AppLink.uploadImagePortrate);
|
||||
// Get.to(() => CarLicensePage());
|
||||
// },
|
||||
kolor: AppColor.greenColor,
|
||||
));
|
||||
} else {}
|
||||
}
|
||||
|
||||
Future<String> uploadImagePortrate() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
final String token = box.read(BoxName.jwt)?.toString().split(AppInformation.addd)[0] ?? '';
|
||||
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
||||
|
||||
var request = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse(AppLink.uploadImagePortrate),
|
||||
);
|
||||
|
||||
request.files.add(
|
||||
http.MultipartFile.fromBytes('image', imagePortrait),
|
||||
);
|
||||
|
||||
request.headers.addAll({
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-FP': fingerPrint,
|
||||
});
|
||||
request.fields['driverID'] = box.read(BoxName.driverID).toString();
|
||||
|
||||
var response = await request.send();
|
||||
var responseData = await response.stream.toBytes();
|
||||
var responseString = String.fromCharCodes(responseData);
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
|
||||
return responseString;
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
// scanDocumentsByApi();
|
||||
// initializeCamera(0);
|
||||
sql.getAllData(TableName.faceDetectTimes).then((value) {
|
||||
if (value.isEmpty) {
|
||||
times = 0;
|
||||
update();
|
||||
// sql.insertData({'faceDetectTimes': 1}, TableName.faceDetectTimes);
|
||||
} else {
|
||||
times = value[0]['faceDetectTimes'];
|
||||
}
|
||||
});
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
|
||||
// class PassportDataExtractor extends GetxController {
|
||||
// @override
|
||||
// void onInit() {
|
||||
// extractPassportData();
|
||||
// super.onInit();
|
||||
// }
|
||||
//
|
||||
// final ImagePicker _imagePicker = ImagePicker();
|
||||
// late final XFile? image;
|
||||
// final TextRecognizer _textRecognizer = TextRecognizer();
|
||||
//
|
||||
// Future<Map<String, dynamic>> extractPassportData() async {
|
||||
// image = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
// update();
|
||||
// if (image == null) {
|
||||
// throw Exception('No image picked');
|
||||
// }
|
||||
//
|
||||
// final InputImage inputImage = InputImage.fromFile(File(image!.path));
|
||||
// final RecognizedText recognisedText =
|
||||
// await _textRecognizer.processImage(inputImage);
|
||||
//
|
||||
// final Map<String, dynamic> extractedData = {};
|
||||
// final List<Map<String, dynamic>> extractedTextWithCoordinates = [];
|
||||
//
|
||||
// for (TextBlock block in recognisedText.blocks) {
|
||||
// for (TextLine line in block.lines) {
|
||||
// final String lineText = line.text;
|
||||
// final Rect lineBoundingBox = line.boundingBox!;
|
||||
//
|
||||
// extractedTextWithCoordinates.add({
|
||||
// 'text': lineText,
|
||||
// 'boundingBox': {
|
||||
// 'left': lineBoundingBox.left,
|
||||
// 'top': lineBoundingBox.top,
|
||||
// 'width': lineBoundingBox.width,
|
||||
// 'height': lineBoundingBox.height,
|
||||
// },
|
||||
// });
|
||||
//
|
||||
// // if (lineText.contains('Passport Number')) {
|
||||
// // final String passportNumber =
|
||||
// // lineText.split('Passport Number')[1].trim();
|
||||
// // extractedData['passportNumber'] = passportNumber;
|
||||
// // }
|
||||
// // if (lineText.contains('Given Names')) {
|
||||
// // final String givenNames = lineText.split('Given Names')[1].trim();
|
||||
// // extractedData['givenNames'] = givenNames;
|
||||
// // }
|
||||
// // if (lineText.contains('Surname')) {
|
||||
// // final String surname = lineText.split('Surname')[1].trim();
|
||||
// // extractedData['surname'] = surname;
|
||||
// // }
|
||||
// // if (lineText.contains('Nationality')) {
|
||||
// // final String nationality = lineText.split('Nationality')[1].trim();
|
||||
// // extractedData['nationality'] = nationality;
|
||||
// // }
|
||||
// // if (lineText.contains('Date of Birth')) {
|
||||
// // final String dob = lineText.split('Date of Birth')[1].trim();
|
||||
// // extractedData['dateOfBirth'] = dob;
|
||||
// // }
|
||||
// // Add more field extraction conditions as needed
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// extractedData['extractedTextWithCoordinates'] =
|
||||
// extractedTextWithCoordinates;
|
||||
// return extractedData;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// class PassportDataController extends GetxController {
|
||||
// PassportDataExtractor passportDataExtractor = PassportDataExtractor();
|
||||
// List<Map<String, dynamic>> extractedTextWithCoordinates = [];
|
||||
//
|
||||
// Future<void> extractDataAndDrawBoundingBoxes() async {
|
||||
// try {
|
||||
// Map<String, dynamic> extractedData =
|
||||
// await passportDataExtractor.extractPassportData();
|
||||
// extractedTextWithCoordinates =
|
||||
// extractedData['extractedTextWithCoordinates'];
|
||||
// update(); // Notify GetX that the state has changed
|
||||
// } catch (e) {
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// class BoundingBoxPainter extends CustomPainter {
|
||||
// final List<Map<String, dynamic>> boundingBoxes;
|
||||
//
|
||||
// BoundingBoxPainter(this.boundingBoxes);
|
||||
//
|
||||
// @override
|
||||
// void paint(Canvas canvas, Size size) {
|
||||
// final Paint paint = Paint()
|
||||
// ..color = Colors.red
|
||||
// ..style = PaintingStyle.stroke
|
||||
// ..strokeWidth = 2.0;
|
||||
//
|
||||
// for (Map<String, dynamic> boundingBox in boundingBoxes) {
|
||||
// double left = boundingBox['left'];
|
||||
// double top = boundingBox['top'];
|
||||
// double width = boundingBox['width'];
|
||||
// double height = boundingBox['height'];
|
||||
//
|
||||
// Rect rect = Rect.fromLTWH(left, top, width, height);
|
||||
// canvas.drawRect(rect, paint);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
114
siro_driver/lib/controller/functions/overlay_permisssion.dart
Executable file
114
siro_driver/lib/controller/functions/overlay_permisssion.dart
Executable file
@@ -0,0 +1,114 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_confetti/flutter_confetti.dart';
|
||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:location/location.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../main.dart';
|
||||
import '../auth/captin/login_captin_controller.dart';
|
||||
import '../home/payment/captain_wallet_controller.dart';
|
||||
|
||||
Future<void> getPermissionOverlay() async {
|
||||
if (Platform.isAndroid) {
|
||||
final bool status = await FlutterOverlayWindow.isPermissionGranted();
|
||||
if (status == false) {
|
||||
MyDialog().getDialog(
|
||||
'Allow overlay permission'.tr,
|
||||
'To display orders instantly, please grant permission to draw over other apps.'
|
||||
.tr,
|
||||
() async {
|
||||
Get.back();
|
||||
await FlutterOverlayWindow.requestPermission();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showDriverGiftClaim(BuildContext context) async {
|
||||
if (box.read(BoxName.is_claimed).toString() == '0' ||
|
||||
box.read(BoxName.is_claimed) == null) {
|
||||
MyDialog().getDialog(
|
||||
'You have gift 300 SYP'.tr, 'This for new registration'.tr, () async {
|
||||
Get.back();
|
||||
var res = await CRUD().post(link: AppLink.updateDriverClaim, payload: {
|
||||
'driverId': box.read(BoxName.driverID),
|
||||
});
|
||||
if (res != 'failure') {
|
||||
Get.find<CaptainWalletController>()
|
||||
.addDriverWallet('new driver', '300', '300');
|
||||
Confetti.launch(
|
||||
context,
|
||||
options:
|
||||
const ConfettiOptions(particleCount: 100, spread: 70, y: 0.6),
|
||||
);
|
||||
box.write(BoxName.is_claimed, '1');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> closeOverlayIfFound() async {
|
||||
if (Platform.isAndroid) {
|
||||
bool isOverlayActive = await FlutterOverlayWindow.isActive();
|
||||
if (isOverlayActive) {
|
||||
await FlutterOverlayWindow.closeOverlay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final location = Location();
|
||||
Future<void> getLocationPermission() async {
|
||||
bool serviceEnabled;
|
||||
PermissionStatus permissionGranted;
|
||||
|
||||
// Check if location services are enabled
|
||||
serviceEnabled = await location.serviceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
serviceEnabled = await location.requestService();
|
||||
if (!serviceEnabled) {
|
||||
// Location services are still not enabled, handle the error
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the app has permission to access location
|
||||
permissionGranted = await location.hasPermission();
|
||||
if (permissionGranted == PermissionStatus.denied) {
|
||||
permissionGranted = await location.requestPermission();
|
||||
if (permissionGranted != PermissionStatus.granted) {
|
||||
// Location permission is still not granted, handle the error
|
||||
permissionGranted = await location.requestPermission();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (permissionGranted.toString() == 'PermissionStatus.granted') {
|
||||
box.write(BoxName.locationPermission, 'true');
|
||||
Get.find<LoginDriverController>().update();
|
||||
}
|
||||
// update();
|
||||
}
|
||||
|
||||
Future<void> getOverLay(String myListString) async {
|
||||
bool isOverlayActive = await FlutterOverlayWindow.isActive();
|
||||
if (isOverlayActive) {
|
||||
await FlutterOverlayWindow.closeOverlay();
|
||||
}
|
||||
await FlutterOverlayWindow.showOverlay(
|
||||
enableDrag: true,
|
||||
flag: OverlayFlag.focusPointer,
|
||||
visibility: NotificationVisibility.visibilityPublic,
|
||||
positionGravity: PositionGravity.auto,
|
||||
height: 700,
|
||||
width: WindowSize.matchParent,
|
||||
startPosition: const OverlayPosition(0, -150),
|
||||
);
|
||||
|
||||
await FlutterOverlayWindow.shareData(myListString);
|
||||
}
|
||||
361
siro_driver/lib/controller/functions/package_info.dart
Executable file
361
siro_driver/lib/controller/functions/package_info.dart
Executable file
@@ -0,0 +1,361 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:jailbreak_root_detection/jailbreak_root_detection.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../constant/info.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import 'encrypt_decrypt.dart';
|
||||
|
||||
Future<void> checkForUpdate(BuildContext context) async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final currentVersion = packageInfo.buildNumber;
|
||||
final version = packageInfo.version;
|
||||
Log.print('version: $version');
|
||||
print('currentVersion is : $currentVersion');
|
||||
// Fetch the latest version from your server
|
||||
String latestVersion = await getPackageInfo();
|
||||
box.write(BoxName.packagInfo, version);
|
||||
|
||||
if (latestVersion.isNotEmpty && latestVersion != currentVersion) {
|
||||
showUpdateDialog(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getPackageInfo() async {
|
||||
final response = await CRUD().get(link: AppLink.packageInfo, payload: {
|
||||
"platform": Platform.isAndroid ? 'android' : 'ios',
|
||||
"appName": AppInformation.appVersion,
|
||||
});
|
||||
|
||||
if (response != 'failure') {
|
||||
return jsonDecode(response)['message'][0]['version'];
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
void showUpdateDialog(BuildContext context) {
|
||||
final String storeUrl = Platform.isAndroid
|
||||
? 'https://play.google.com/store/apps/details?id=com.intaleq_driver'
|
||||
: 'https://apps.apple.com/jo/app/intaleq-driver/id6482995159';
|
||||
|
||||
showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierColor: Colors.black.withOpacity(0.5),
|
||||
pageBuilder: (_, __, ___) {
|
||||
return BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||
child: Center(
|
||||
child: AlertDialog(
|
||||
// Using AlertDialog for a more Material Design look
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(16)), // More rounded corners
|
||||
elevation: 4, // Add a bit more elevation
|
||||
contentPadding: EdgeInsets.zero, // Remove default content padding
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20.0),
|
||||
child: Image.asset(
|
||||
'assets/images/logo.png',
|
||||
height: 72, // Slightly larger logo
|
||||
width: 72,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Text(
|
||||
'Update Available'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
// Use theme's title style
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Text(
|
||||
'A new version of the app is available. Please update to the latest version.'
|
||||
.tr, // More encouraging message
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
// Use theme's body style
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
// Using TextButton for "Cancel"
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.grey,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text('Cancel'.tr),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 48,
|
||||
child: VerticalDivider(width: 0), // Using VerticalDivider
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
// Using ElevatedButton for "Update"
|
||||
onPressed: () async {
|
||||
if (await canLaunchUrl(Uri.parse(storeUrl))) {
|
||||
await launchUrl(Uri.parse(storeUrl));
|
||||
}
|
||||
if (context.mounted) Navigator.pop(context);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor
|
||||
.primaryColor, // Use theme's primary color
|
||||
foregroundColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary, // Use theme's onPrimary color
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text('Update'.tr),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
transitionBuilder: (_, animation, __, child) {
|
||||
return ScaleTransition(
|
||||
scale: CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOutCubic, // More natural curve
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class DeviceHelper {
|
||||
static Future<String> getDeviceFingerprint() async {
|
||||
await EncryptionHelper.initialize();
|
||||
final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
|
||||
var deviceData;
|
||||
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo;
|
||||
deviceData = androidInfo.toMap();
|
||||
} else if (Platform.isIOS) {
|
||||
IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo;
|
||||
deviceData = iosInfo.toMap();
|
||||
} else {
|
||||
throw UnsupportedError('Unsupported platform');
|
||||
}
|
||||
|
||||
final String deviceId = Platform.isAndroid
|
||||
? deviceData['id'] ?? deviceData['androidId'] ?? deviceData['fingerprint'] ?? 'unknown'
|
||||
: deviceData['identifierForVendor'] ?? 'unknown';
|
||||
|
||||
final String deviceModel = deviceData['model'] ?? 'unknown';
|
||||
|
||||
final String fingerprint =
|
||||
EncryptionHelper.instance.encryptData('${deviceId}_$deviceModel');
|
||||
|
||||
box.write(BoxName.deviceFingerprint, fingerprint);
|
||||
return (fingerprint);
|
||||
} catch (e) {
|
||||
debugPrint('Error generating device fingerprint: $e');
|
||||
throw Exception('Failed to generate device fingerprint: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SecurityHelper {
|
||||
/// Performs security checks and handles potential risks
|
||||
static Future<void> performSecurityChecks() async {
|
||||
bool isNotTrust = false;
|
||||
bool isJailBroken = false;
|
||||
bool isRealDevice = true;
|
||||
bool isOnExternalStorage = false;
|
||||
bool checkForIssues = false;
|
||||
bool isDevMode = false;
|
||||
bool isTampered = false;
|
||||
String bundleId = "";
|
||||
|
||||
try {
|
||||
isNotTrust = await JailbreakRootDetection.instance.isNotTrust;
|
||||
isJailBroken = await JailbreakRootDetection.instance.isJailBroken;
|
||||
isRealDevice = await JailbreakRootDetection.instance.isRealDevice;
|
||||
|
||||
// This method is only relevant/implemented for Android
|
||||
if (Platform.isAndroid) {
|
||||
isOnExternalStorage =
|
||||
await JailbreakRootDetection.instance.isOnExternalStorage;
|
||||
}
|
||||
|
||||
List<JailbreakIssue> issues =
|
||||
await JailbreakRootDetection.instance.checkForIssues;
|
||||
checkForIssues = issues.isNotEmpty;
|
||||
|
||||
isDevMode = await JailbreakRootDetection.instance.isDevMode;
|
||||
|
||||
// Get Bundle ID
|
||||
PackageInfo packageInfo = await PackageInfo.fromPlatform();
|
||||
bundleId = packageInfo.packageName;
|
||||
if (bundleId.isNotEmpty) {
|
||||
isTampered = await JailbreakRootDetection.instance.isTampered(bundleId);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error during security checks: $e");
|
||||
// Consider handling specific exceptions, not just general errors.
|
||||
}
|
||||
|
||||
// Save values to storage (using GetStorage)
|
||||
await box.write('isNotTrust', isNotTrust); // Use await for write operations
|
||||
await box.write('isTampered', isTampered); // Use await
|
||||
await box.write('isJailBroken', isJailBroken); // Use await
|
||||
|
||||
// debugPrint("Security Check Results:");
|
||||
// debugPrint("isNotTrust: $isNotTrust");
|
||||
// debugPrint("isJailBroken: $isJailBroken");
|
||||
// debugPrint("isRealDevice: $isRealDevice");
|
||||
// debugPrint("isOnExternalStorage: $isOnExternalStorage");
|
||||
// debugPrint("checkForIssues: $checkForIssues");
|
||||
// debugPrint("isDevMode: $isDevMode");
|
||||
// debugPrint("isTampered: $isTampered");
|
||||
// debugPrint("Bundle ID: $bundleId"); // Print the bundle ID
|
||||
|
||||
// Check for security risks and potentially show a warning
|
||||
if (isJailBroken || isRealDevice == false || isTampered) {
|
||||
// print("security_warning".tr); //using easy_localization
|
||||
// Use a more robust approach to show a warning, like a dialog:
|
||||
_showSecurityWarning();
|
||||
} else {
|
||||
box.write(BoxName.security_check, 'passed');
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes all app data
|
||||
static Future<void> clearAllData() async {
|
||||
//await storage.deleteAll(); // What's 'storage'? Be specific. Likely GetStorage as well.
|
||||
await box.erase(); // Clear GetStorage data
|
||||
exit(0); // This will terminate the app. Be VERY careful with this.
|
||||
}
|
||||
|
||||
// static void _showSecurityWarning() {
|
||||
// // Show a dialog, navigate to an error screen, etc.
|
||||
// // Example using Get.dialog (if you use GetX):
|
||||
//
|
||||
// Get.dialog(
|
||||
// AlertDialog(
|
||||
// title: Text("Security Warning".tr), // Or use localized string
|
||||
// content: Text(
|
||||
// "Potential security risks detected. The application may not function correctly."
|
||||
// .tr), //Or use localized string
|
||||
// actions: [
|
||||
// TextButton(
|
||||
// onPressed: () async {
|
||||
// await storage.deleteAll();
|
||||
// await box.erase();
|
||||
// Get.back(); // Close the dialog
|
||||
// // Or, if you really must, exit the app (but give the user a chance!)
|
||||
// exit(0);
|
||||
// },
|
||||
// child: Text("OK"), // Or use a localized string
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// barrierDismissible: false, // Prevent closing by tapping outside
|
||||
// );
|
||||
// }
|
||||
static void _showSecurityWarning() {
|
||||
// Use an RxInt to track the remaining seconds. This is the KEY!
|
||||
RxInt secondsRemaining = 10.obs;
|
||||
|
||||
Get.dialog(
|
||||
CupertinoAlertDialog(
|
||||
title: Text("Security Warning".tr),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Obx(() => Text(
|
||||
"Potential security risks detected. The application will close in @seconds seconds."
|
||||
.trParams({
|
||||
// Use trParams for placeholders
|
||||
'seconds': secondsRemaining.value.toString(),
|
||||
}),
|
||||
// Wrap the Text widget in Obx
|
||||
)),
|
||||
SizedBox(height: 24), // More spacing before the progress bar
|
||||
Obx(() => SizedBox(
|
||||
width: double.infinity, // Make progress bar full width
|
||||
child: CupertinoActivityIndicator(
|
||||
// in case of loading
|
||||
radius: 15,
|
||||
animating: true,
|
||||
))),
|
||||
SizedBox(height: 8),
|
||||
Obx(() => ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8), // Rounded corners
|
||||
child: LinearProgressIndicator(
|
||||
value: secondsRemaining.value / 10,
|
||||
backgroundColor: Colors.grey.shade300, // Lighter background
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
CupertinoColors.systemRed), // iOS-style red
|
||||
minHeight: 8, // Slightly thicker progress bar
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
|
||||
Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
secondsRemaining.value--;
|
||||
if (secondsRemaining.value <= 0) {
|
||||
timer.cancel();
|
||||
// Get.back();
|
||||
_clearDataAndExit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> _clearDataAndExit() async {
|
||||
await storage.deleteAll();
|
||||
await box.erase();
|
||||
exit(0); // Exit the app
|
||||
print('exit');
|
||||
}
|
||||
}
|
||||
43
siro_driver/lib/controller/functions/performance_test.dart
Normal file
43
siro_driver/lib/controller/functions/performance_test.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'dart:io';
|
||||
|
||||
class PerformanceTester {
|
||||
/// ✅ فحص سرعة الكتابة إلى التخزين (Storage Write Speed) بوحدة MB/s
|
||||
static Future<double> testStorageWriteSpeed() async {
|
||||
try {
|
||||
final tempDir = Directory.systemTemp;
|
||||
final testFile = File('${tempDir.path}/speed_test.txt');
|
||||
final data = List<int>.filled(1024 * 1024 * 5, 0); // 5MB
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
await testFile.writeAsBytes(data, flush: true);
|
||||
stopwatch.stop();
|
||||
|
||||
await testFile.delete();
|
||||
|
||||
double seconds = stopwatch.elapsedMilliseconds / 1000;
|
||||
if (seconds == 0) seconds = 0.001;
|
||||
|
||||
final speed = 5 / seconds;
|
||||
return double.parse(speed.toStringAsFixed(2));
|
||||
} catch (e) {
|
||||
print("❌ Storage write error: $e");
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// ✅ فحص سرعة المعالج (CPU Compute Speed) بوحدة الثواني
|
||||
static Future<double> testCPUSpeed() async {
|
||||
try {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
double x = 0;
|
||||
for (int i = 0; i < 100000000; i++) {
|
||||
x += i * 0.000001;
|
||||
}
|
||||
stopwatch.stop();
|
||||
return stopwatch.elapsedMilliseconds / 1000.0;
|
||||
} catch (e) {
|
||||
print("❌ CPU compute error: $e");
|
||||
return 999.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
siro_driver/lib/controller/functions/remove_account.dart
Executable file
8
siro_driver/lib/controller/functions/remove_account.dart
Executable file
@@ -0,0 +1,8 @@
|
||||
// import 'package:ride/controller/functions/crud.dart';
|
||||
|
||||
// class RemoveAccount {
|
||||
|
||||
// void removeAccount()async{
|
||||
// var res=await CRUD().post(link: link)
|
||||
// }
|
||||
// }
|
||||
25
siro_driver/lib/controller/functions/scan_id_card.dart
Executable file
25
siro_driver/lib/controller/functions/scan_id_card.dart
Executable file
@@ -0,0 +1,25 @@
|
||||
// import 'package:credit_card_scanner/credit_card_scanner.dart';
|
||||
// import 'package:get/get.dart';
|
||||
//
|
||||
// class ScanIdCard extends GetxController {
|
||||
// CardDetails? _cardDetails;
|
||||
// CardScanOptions scanOptions = const CardScanOptions(
|
||||
// scanCardHolderName: true,
|
||||
// enableDebugLogs: true,
|
||||
// validCardsToScanBeforeFinishingScan: 5,
|
||||
// possibleCardHolderNamePositions: [
|
||||
// CardHolderNameScanPosition.aboveCardNumber,
|
||||
// ],
|
||||
// );
|
||||
//
|
||||
// Future<void> scanCard() async {
|
||||
// final CardDetails? cardDetails =
|
||||
// await CardScanner.scanCard(scanOptions: scanOptions);
|
||||
// if (cardDetails == null) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// _cardDetails = cardDetails;
|
||||
// update();
|
||||
// }
|
||||
// }
|
||||
100
siro_driver/lib/controller/functions/secure_storage.dart
Executable file
100
siro_driver/lib/controller/functions/secure_storage.dart
Executable file
@@ -0,0 +1,100 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:secure_string_operations/secure_string_operations.dart';
|
||||
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
import 'package:siro_driver/controller/functions/encrypt_decrypt.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/char_map.dart';
|
||||
import '../../constant/info.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import 'crud.dart';
|
||||
|
||||
class SecureStorage {
|
||||
final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
||||
|
||||
void saveData(String key, value) async {
|
||||
await _storage.write(key: key, value: value);
|
||||
}
|
||||
|
||||
Future<String?> readData(String boxName) async {
|
||||
final String? value = await _storage.read(key: boxName);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
const List<String> keysToFetch = [
|
||||
'serverPHP',
|
||||
'seferAlexandriaServer',
|
||||
'seferPaymentServer',
|
||||
'seferCairoServer',
|
||||
'seferGizaServer',
|
||||
];
|
||||
|
||||
class AppInitializer {
|
||||
List<Map<String, dynamic>> links = [];
|
||||
|
||||
Future<void> initializeApp() async {
|
||||
if (box.read(BoxName.jwt) == null) {
|
||||
await LoginDriverController().getJWT();
|
||||
} else {
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(AppInformation.addd)[0];
|
||||
bool isTokenValid = false;
|
||||
try {
|
||||
final parts = token.split('.');
|
||||
if (parts.length == 3) {
|
||||
String payload = parts[1];
|
||||
switch (payload.length % 4) {
|
||||
case 2: payload += '=='; break;
|
||||
case 3: payload += '='; break;
|
||||
}
|
||||
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
|
||||
final exp = decoded['exp'];
|
||||
if (exp != null) {
|
||||
isTokenValid = DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
if (!isTokenValid) {
|
||||
await LoginDriverController().getJWT();
|
||||
}
|
||||
}
|
||||
|
||||
// await getKey();
|
||||
}
|
||||
|
||||
Future<void> getAIKey(String key1) async {
|
||||
var res =
|
||||
await CRUD().get(link: AppLink.getapiKey, payload: {"keyName": key1});
|
||||
|
||||
if (res != 'failure') {
|
||||
var d = jsonDecode(res)['message'];
|
||||
final rawValue = d[key1].toString();
|
||||
|
||||
// ✅ اكتبها في storage
|
||||
await storage.write(key: key1, value: rawValue);
|
||||
|
||||
await Future.delayed(Duration.zero);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getKey() async {
|
||||
try {
|
||||
var res =
|
||||
await CRUD().get(link: AppLink.getLocationAreaLinks, payload: {});
|
||||
if (res != 'failure') {
|
||||
links = List<Map<String, dynamic>>.from(jsonDecode(res)['message']);
|
||||
|
||||
await box.write(BoxName.locationName, links);
|
||||
await box.write(BoxName.basicLink, (links[0]['server_link']));
|
||||
await box.write(links[2]['name'], (links[2]['server_link']));
|
||||
await box.write(links[1]['name'], (links[3]['server_link']));
|
||||
await box.write(links[3]['name'], (links[1]['server_link']));
|
||||
await box.write(BoxName.paymentLink, (links[4]['server_link']));
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
51
siro_driver/lib/controller/functions/security_checks.dart
Executable file
51
siro_driver/lib/controller/functions/security_checks.dart
Executable file
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../main.dart';
|
||||
|
||||
class SecurityChecks {
|
||||
static const platform = MethodChannel(
|
||||
'com.intaleq_driver/security'); // Choose a unique channel name
|
||||
|
||||
static Future<bool> isDeviceCompromised() async {
|
||||
try {
|
||||
final bool result = await platform
|
||||
.invokeMethod('isNativeRooted'); // Invoke the native method
|
||||
return result;
|
||||
} on PlatformException catch (e) {
|
||||
print("Failed to check security status: ${e.message}");
|
||||
return true; // Treat platform errors as a compromised device (for safety)
|
||||
}
|
||||
}
|
||||
|
||||
static isDeviceRootedFromNative(BuildContext context) async {
|
||||
bool compromised = await isDeviceCompromised();
|
||||
if (compromised) {
|
||||
showDialog(
|
||||
barrierDismissible: false,
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text("Security Warning".tr),
|
||||
content: Text(
|
||||
"Your device appears to be compromised. The app will now close."
|
||||
.tr),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
SystemNavigator.pop(); // Close the app
|
||||
},
|
||||
child: Text("OK"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
box.write(BoxName.security_check, 'passed');
|
||||
|
||||
// Continue with normal app flow
|
||||
print("Device is secure.");
|
||||
}
|
||||
}
|
||||
}
|
||||
104
siro_driver/lib/controller/functions/sms_egypt_controller.dart
Executable file
104
siro_driver/lib/controller/functions/sms_egypt_controller.dart
Executable file
@@ -0,0 +1,104 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/info.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/auth/captin/register_captin_controller.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../auth/captin/login_captin_controller.dart';
|
||||
import 'encrypt_decrypt.dart';
|
||||
|
||||
class SmsEgyptController extends GetxController {
|
||||
var headers = {'Content-Type': 'application/json'};
|
||||
|
||||
Future<String> getSender() async {
|
||||
var res = await CRUD().get(link: AppLink.getSender, payload: {});
|
||||
if (res != 'failure') {
|
||||
var d = jsonDecode(res)['message'][0]['senderId'].toString();
|
||||
return d;
|
||||
} else {
|
||||
return "Sefer Egy";
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> sendSmsEgypt(String phone) async {
|
||||
String sender = await getSender();
|
||||
var body = jsonEncode({"receiver": "2$phone"});
|
||||
|
||||
var res = await http.post(
|
||||
Uri.parse(AppLink.sendSms),
|
||||
body: body,
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (jsonDecode(res.body)['message'].toString() != "Success") {
|
||||
await CRUD().post(link: AppLink.updatePhoneInvalidSMS, payload: {
|
||||
"phone_number":
|
||||
('+2${Get.find<RegisterCaptainController>().phoneController.text}')
|
||||
});
|
||||
box.write(BoxName.phoneDriver,
|
||||
('+2${Get.find<RegisterCaptainController>().phoneController.text}'));
|
||||
box.write(BoxName.phoneVerified, '1');
|
||||
|
||||
await Get.put(LoginDriverController()).loginWithGoogleCredential(
|
||||
box.read(BoxName.driverID).toString(),
|
||||
(box.read(BoxName.emailDriver).toString()),
|
||||
);
|
||||
} else {
|
||||
Get.defaultDialog(
|
||||
title: 'You will receive code in sms message'.tr,
|
||||
middleText: '',
|
||||
confirm: MyElevatedButton(
|
||||
title: 'OK'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Future checkCredit(String phone, otp) async {
|
||||
var res = await http.post(
|
||||
Uri.parse(AppLink.checkCredit),
|
||||
body: {
|
||||
"username": AppInformation.appName,
|
||||
"password": AK.smsPasswordEgypt,
|
||||
},
|
||||
headers: headers,
|
||||
);
|
||||
}
|
||||
|
||||
Future sendSmsWithValidaty(String phone, otp) async {
|
||||
var res = await http.post(
|
||||
Uri.parse(AppLink.checkCredit),
|
||||
body: {
|
||||
"username": 'Sefer',
|
||||
"password": AK.smsPasswordEgypt,
|
||||
"message": "This is an example SMS message.",
|
||||
"language": box.read(BoxName.lang) == 'en' ? "e" : 'r',
|
||||
"sender": "Sefer", //"Kazumi", // todo add sefer sender name
|
||||
"receiver": "2$phone",
|
||||
"validity": "10",
|
||||
"StartTime": DateTime.now().toString() // "1/1/2024 10:00:00"
|
||||
},
|
||||
headers: headers,
|
||||
);
|
||||
}
|
||||
|
||||
Future sendSmsStatus(String smsid) async {
|
||||
var res = await http.post(
|
||||
Uri.parse(AppLink.checkCredit),
|
||||
body: {
|
||||
"username": AppInformation.appName,
|
||||
"password": AK.smsPasswordEgypt,
|
||||
"smsid": smsid //"00b77dfc-5b8f-474d-9def-9f0158b70f98"
|
||||
},
|
||||
headers: headers,
|
||||
);
|
||||
}
|
||||
}
|
||||
35
siro_driver/lib/controller/functions/toast.dart
Executable file
35
siro_driver/lib/controller/functions/toast.dart
Executable file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
|
||||
class Toast {
|
||||
static void show(BuildContext context, String message, Color color) {
|
||||
final snackBar = SnackBar(
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
backgroundColor: color,
|
||||
elevation: 3,
|
||||
content: Text(
|
||||
message,
|
||||
style: AppStyle.title.copyWith(color: AppColor.secondaryColor),
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
animation: const AlwaysStoppedAnimation(1.0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10.0), // Custom border radius
|
||||
),
|
||||
width: Get.width * .8,
|
||||
// shape: const StadiumBorder(
|
||||
// side: BorderSide(
|
||||
// color: AppColor.secondaryColor,
|
||||
// width: 1.0,
|
||||
// style: BorderStyle.solid,
|
||||
// )),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
snackBar,
|
||||
);
|
||||
}
|
||||
}
|
||||
93
siro_driver/lib/controller/functions/tts.dart
Executable file
93
siro_driver/lib/controller/functions/tts.dart
Executable file
@@ -0,0 +1,93 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter_tts/flutter_tts.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
|
||||
class TextToSpeechController extends GetxController {
|
||||
final FlutterTts flutterTts = FlutterTts();
|
||||
bool isSpeaking = false;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
initTts();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
flutterTts.stop();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// --- 1. تهيئة المحرك بإعدادات قوية للملاحة ---
|
||||
Future<void> initTts() async {
|
||||
try {
|
||||
// جلب اللغة المحفوظة أو استخدام العربية كافتراضي
|
||||
String lang = box.read(BoxName.lang) ?? 'ar-SA';
|
||||
|
||||
// تصحيح صيغة اللغة إذا لزم الأمر
|
||||
if (lang == 'ar') lang = 'ar-SA';
|
||||
if (lang == 'en') lang = 'en-US';
|
||||
|
||||
await flutterTts.setLanguage(lang);
|
||||
await flutterTts.setSpeechRate(0.5); // سرعة متوسطة وواضحة
|
||||
await flutterTts.setVolume(1.0);
|
||||
await flutterTts.setPitch(1.0);
|
||||
|
||||
// إعدادات خاصة لضمان عمل الصوت مع الملاحة (خاصة للآيفون)
|
||||
if (Platform.isIOS) {
|
||||
await flutterTts
|
||||
.setIosAudioCategory(IosTextToSpeechAudioCategory.playback, [
|
||||
IosTextToSpeechAudioCategoryOptions.mixWithOthers,
|
||||
IosTextToSpeechAudioCategoryOptions.duckOthers
|
||||
]);
|
||||
}
|
||||
|
||||
// الاستماع لحالة الانتهاء
|
||||
flutterTts.setCompletionHandler(() {
|
||||
isSpeaking = false;
|
||||
update();
|
||||
});
|
||||
|
||||
flutterTts.setStartHandler(() {
|
||||
isSpeaking = true;
|
||||
update();
|
||||
});
|
||||
} catch (e) {
|
||||
print("TTS Init Error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. دالة التحدث (تقاطع الكلام القديم) ---
|
||||
Future<void> speakText(String text) async {
|
||||
if (text.isEmpty) return;
|
||||
|
||||
try {
|
||||
// إيقاف أي كلام حالي لضمان نطق التوجيه الجديد فوراً (أهم للملاحة)
|
||||
await flutterTts.stop();
|
||||
|
||||
var result = await flutterTts.speak(text);
|
||||
if (result == 1) {
|
||||
isSpeaking = true;
|
||||
update();
|
||||
}
|
||||
} catch (error) {
|
||||
// لا تعرض سناك بار هنا لتجنب إزعاج السائق أثناء القيادة
|
||||
print('Failed to speak text: $error');
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. دالة الإيقاف (ضرورية لزر الكتم) ---
|
||||
Future<void> stop() async {
|
||||
try {
|
||||
var result = await flutterTts.stop();
|
||||
if (result == 1) {
|
||||
isSpeaking = false;
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error stopping TTS: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
22
siro_driver/lib/controller/functions/twilio_service.dart
Executable file
22
siro_driver/lib/controller/functions/twilio_service.dart
Executable file
@@ -0,0 +1,22 @@
|
||||
// import 'package:ride/constant/credential.dart';
|
||||
// import 'package:twilio_flutter/twilio_flutter.dart';
|
||||
//
|
||||
// class TwilioSMS {
|
||||
// TwilioFlutter twilioFlutter = TwilioFlutter(
|
||||
// accountSid: AppCredintials.accountSIDTwillo,
|
||||
// authToken: AppCredintials.authTokenTwillo,
|
||||
// twilioNumber: '+962 7 9858 3052');
|
||||
//
|
||||
// Future<void> sendSMS({
|
||||
// required String recipientPhoneNumber,
|
||||
// required String message,
|
||||
// }) async {
|
||||
// try {
|
||||
// await twilioFlutter.sendSMS(
|
||||
// toNumber: recipientPhoneNumber,
|
||||
// messageBody: message,
|
||||
// );
|
||||
// } catch (e) {
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
537
siro_driver/lib/controller/functions/upload_image.dart
Executable file
537
siro_driver/lib/controller/functions/upload_image.dart
Executable file
@@ -0,0 +1,537 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image_cropper/image_cropper.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
import 'package:flutter_image_compress/flutter_image_compress.dart';
|
||||
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/info.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import 'encrypt_decrypt.dart';
|
||||
|
||||
class ImageController extends GetxController {
|
||||
File? myImage;
|
||||
bool isloading = false;
|
||||
CroppedFile? croppedFile;
|
||||
final picker = ImagePicker();
|
||||
var image;
|
||||
|
||||
Future<img.Image> detectAndCropDocument(File imageFile) async {
|
||||
img.Image? image = img.decodeImage(await imageFile.readAsBytes());
|
||||
if (image == null) throw Exception('Unable to decode image');
|
||||
|
||||
int left = image.width, top = image.height, right = 0, bottom = 0;
|
||||
|
||||
// Threshold for considering a pixel as part of the document (adjust as needed)
|
||||
const int threshold = 240;
|
||||
|
||||
for (int y = 0; y < image.height; y++) {
|
||||
for (int x = 0; x < image.width; x++) {
|
||||
final pixel = image.getPixel(x, y);
|
||||
final luminance = img.getLuminance(pixel);
|
||||
|
||||
if (luminance < threshold) {
|
||||
left = x < left ? x : left;
|
||||
top = y < top ? y : top;
|
||||
right = x > right ? x : right;
|
||||
bottom = y > bottom ? y : bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a small padding
|
||||
left = (left - 5).clamp(0, image.width);
|
||||
top = (top - 5).clamp(0, image.height);
|
||||
right = (right + 5).clamp(0, image.width);
|
||||
bottom = (bottom + 5).clamp(0, image.height);
|
||||
|
||||
return img.copyCrop(image,
|
||||
x: left, y: top, width: right - left, height: bottom - top);
|
||||
}
|
||||
|
||||
Future<File> rotateImageIfNeeded(File imageFile) async {
|
||||
img.Image croppedDoc = await detectAndCropDocument(imageFile);
|
||||
|
||||
// Check if the document is in portrait orientation
|
||||
bool isPortrait = croppedDoc.height > croppedDoc.width;
|
||||
|
||||
img.Image processedImage;
|
||||
if (isPortrait) {
|
||||
// Rotate the image by 90 degrees clockwise
|
||||
processedImage = img.copyRotate(croppedDoc, angle: 90);
|
||||
} else {
|
||||
processedImage = croppedDoc;
|
||||
}
|
||||
|
||||
// Get temporary directory
|
||||
final tempDir = await path_provider.getTemporaryDirectory();
|
||||
final tempPath = tempDir.path;
|
||||
|
||||
// Create the processed image file
|
||||
File processedFile = File('$tempPath/processed_image.jpg');
|
||||
await processedFile.writeAsBytes(img.encodeJpg(processedImage));
|
||||
|
||||
return processedFile;
|
||||
}
|
||||
|
||||
Future<File> rotateImage(File imageFile) async {
|
||||
// Read the image file
|
||||
img.Image? image = img.decodeImage(await imageFile.readAsBytes());
|
||||
|
||||
if (image == null) return imageFile;
|
||||
|
||||
// Rotate the image by 90 degrees clockwise
|
||||
img.Image rotatedImage = img.copyRotate(image, angle: 90);
|
||||
|
||||
// Get temporary directory
|
||||
final tempDir = await path_provider.getTemporaryDirectory();
|
||||
final tempPath = tempDir.path;
|
||||
|
||||
// Create the rotated image file
|
||||
File rotatedFile = File('$tempPath/rotated_image.jpg');
|
||||
await rotatedFile.writeAsBytes(img.encodeJpg(rotatedImage));
|
||||
|
||||
return rotatedFile;
|
||||
}
|
||||
|
||||
choosImage(String link, String imageType) async {
|
||||
try {
|
||||
final pickedImage = await picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
preferredCameraDevice: CameraDevice.rear,
|
||||
);
|
||||
|
||||
if (pickedImage == null) return;
|
||||
|
||||
image = File(pickedImage.path);
|
||||
|
||||
croppedFile = await ImageCropper().cropImage(
|
||||
sourcePath: image!.path,
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: 'Cropper'.tr,
|
||||
toolbarColor: AppColor.blueColor,
|
||||
toolbarWidgetColor: AppColor.yellowColor,
|
||||
initAspectRatio: CropAspectRatioPreset.original,
|
||||
lockAspectRatio: false,
|
||||
),
|
||||
IOSUiSettings(
|
||||
title: 'Cropper'.tr,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (croppedFile == null) return;
|
||||
|
||||
myImage = File(croppedFile!.path);
|
||||
isloading = true;
|
||||
update();
|
||||
|
||||
// Rotate the compressed image
|
||||
File processedImage = await rotateImageIfNeeded(File(croppedFile!.path));
|
||||
File compressedImage = await compressImage(processedImage);
|
||||
|
||||
print('link =$link');
|
||||
// Log.print('link: ${link}');
|
||||
//n8u22456
|
||||
await uploadImage(
|
||||
compressedImage,
|
||||
{
|
||||
'driverID':
|
||||
box.read(BoxName.driverID) ?? box.read(BoxName.passengerID),
|
||||
'imageType': imageType,
|
||||
},
|
||||
link,
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error in choosImage: $e');
|
||||
mySnackeBarError('Image Upload Failed'.tr);
|
||||
// Get.snackbar('Image Upload Failed'.tr, e.toString(),
|
||||
// backgroundColor: AppColor.primaryColor);
|
||||
} finally {
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
choosImageNewCAr(String link, String imageType) async {
|
||||
try {
|
||||
final pickedImage = await picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
preferredCameraDevice: CameraDevice.rear,
|
||||
);
|
||||
|
||||
if (pickedImage == null) return;
|
||||
|
||||
image = File(pickedImage.path);
|
||||
|
||||
croppedFile = await ImageCropper().cropImage(
|
||||
sourcePath: image!.path,
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: 'Cropper'.tr,
|
||||
toolbarColor: AppColor.blueColor,
|
||||
toolbarWidgetColor: AppColor.yellowColor,
|
||||
initAspectRatio: CropAspectRatioPreset.original,
|
||||
lockAspectRatio: false,
|
||||
),
|
||||
IOSUiSettings(
|
||||
title: 'Cropper'.tr,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (croppedFile == null) return;
|
||||
|
||||
myImage = File(croppedFile!.path);
|
||||
isloading = true;
|
||||
update();
|
||||
|
||||
// Rotate the compressed image
|
||||
File processedImage = await rotateImageIfNeeded(File(croppedFile!.path));
|
||||
File compressedImage = await compressImage(processedImage);
|
||||
|
||||
print('link =$link');
|
||||
// Log.print('link: ${link}');
|
||||
//n8u22456
|
||||
await uploadNewCar(
|
||||
compressedImage,
|
||||
{
|
||||
'driverID': box.read(BoxName.driverID) +
|
||||
'_' +
|
||||
DateTime.now().toIso8601String(),
|
||||
'imageType': imageType,
|
||||
},
|
||||
link,
|
||||
);
|
||||
} catch (e) {
|
||||
print('Error in choosImage: $e');
|
||||
// Get.snackbar('Image Upload Failed'.tr, e.toString(),
|
||||
// backgroundColor: AppColor.primaryColor);
|
||||
mySnackeBarError('Image Upload Failed'.tr);
|
||||
} finally {
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
// choosFaceFromDriverLicense(String link, String imageType) async {
|
||||
// final pickedImage = await picker.pickImage(
|
||||
// source: ImageSource.camera,
|
||||
// preferredCameraDevice: CameraDevice.rear,
|
||||
// );
|
||||
|
||||
// if (pickedImage == null) return;
|
||||
|
||||
// image = File(pickedImage.path);
|
||||
|
||||
// File? processedImage;
|
||||
|
||||
// // For face images, use face detection and cropping
|
||||
// processedImage = await detectAndCropFace(image!);
|
||||
// if (processedImage == null) {
|
||||
// Get.snackbar('Face Detection Failed', 'No face detected in the image.');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// isloading = true;
|
||||
// update();
|
||||
|
||||
// File compressedImage = await compressImage(processedImage);
|
||||
|
||||
// try {
|
||||
// await uploadImage(
|
||||
// compressedImage,
|
||||
// {
|
||||
// 'driverID': box.read(BoxName.driverID).toString(),
|
||||
// 'imageType': imageType
|
||||
// },
|
||||
// link,
|
||||
// );
|
||||
// } catch (e) {
|
||||
// Get.snackbar('Image Upload Failed'.tr, e.toString(),
|
||||
// backgroundColor: AppColor.redColor);
|
||||
// } finally {
|
||||
// isloading = false;
|
||||
// update();
|
||||
// }
|
||||
// }
|
||||
|
||||
choosFace(String link, String imageType) async {
|
||||
final pickedImage = await picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
preferredCameraDevice: CameraDevice.front,
|
||||
);
|
||||
if (pickedImage != null) {
|
||||
image = File(pickedImage.path);
|
||||
isloading = true;
|
||||
update();
|
||||
// Compress the image
|
||||
File compressedImage = await compressImage(File(pickedImage.path));
|
||||
|
||||
// Save the picked image directly
|
||||
// File savedImage = File(pickedImage.path);
|
||||
print('link =$link');
|
||||
try {
|
||||
await uploadImage(
|
||||
compressedImage,
|
||||
{
|
||||
'driverID':
|
||||
box.read(BoxName.driverID) ?? box.read(BoxName.passengerID),
|
||||
'imageType': imageType
|
||||
},
|
||||
link,
|
||||
);
|
||||
} catch (e) {
|
||||
mySnackeBarError('Image Upload Failed'.tr);
|
||||
// Get.snackbar('Image Upload Failed'.tr, e.toString(),
|
||||
// backgroundColor: AppColor.redColor);
|
||||
} finally {
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uploadImage(File file, Map data, String link) async {
|
||||
final String token = r(box.read(BoxName.jwt)).split(AppInformation.addd)[0];
|
||||
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
||||
|
||||
var request = http.MultipartRequest('POST', Uri.parse(link));
|
||||
Log.print('uploadImage -> $link');
|
||||
|
||||
request.headers.addAll({
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-FP': fingerPrint,
|
||||
});
|
||||
|
||||
var length = await file.length();
|
||||
var stream = http.ByteStream(file.openRead());
|
||||
request.files.add(
|
||||
http.MultipartFile(
|
||||
'image',
|
||||
stream,
|
||||
length,
|
||||
filename: '${box.read(BoxName.driverID)}.jpg',
|
||||
),
|
||||
);
|
||||
data.forEach((key, value) {
|
||||
request.fields[key] = value;
|
||||
});
|
||||
|
||||
var myrequest = await request.send();
|
||||
var res = await http.Response.fromStream(myrequest);
|
||||
Log.print('uploadImage response [${res.statusCode}]: ${res.body}');
|
||||
if (res.statusCode == 200) {
|
||||
return jsonDecode(res.body);
|
||||
} else {
|
||||
throw Exception('Failed to upload image: ${res.statusCode} - ${res.body}');
|
||||
}
|
||||
}
|
||||
|
||||
uploadNewCar(File file, Map data, String link) async {
|
||||
final String token = r(box.read(BoxName.jwt)).split(AppInformation.addd)[0];
|
||||
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
||||
|
||||
var request = http.MultipartRequest('POST', Uri.parse(link));
|
||||
request.headers.addAll({
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-FP': fingerPrint,
|
||||
});
|
||||
|
||||
var length = await file.length();
|
||||
var stream = http.ByteStream(file.openRead());
|
||||
request.files.add(
|
||||
http.MultipartFile(
|
||||
'image',
|
||||
stream,
|
||||
length,
|
||||
filename: '${box.read(BoxName.driverID)}.jpg',
|
||||
),
|
||||
);
|
||||
data.forEach((key, value) {
|
||||
request.fields[key] = value;
|
||||
});
|
||||
|
||||
var myrequest = await request.send();
|
||||
var res = await http.Response.fromStream(myrequest);
|
||||
Log.print('uploadNewCar response [${res.statusCode}]: ${res.body}');
|
||||
if (res.statusCode == 200) {
|
||||
return jsonDecode(res.body);
|
||||
} else {
|
||||
throw Exception('Failed to upload image: ${res.statusCode} - ${res.body}');
|
||||
}
|
||||
}
|
||||
|
||||
choosImagePicture(String link, String imageType) async {
|
||||
final pickedImage = await picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
// preferredCameraDevice: CameraDevice.rear,
|
||||
// maxHeight: Get.height * .3,
|
||||
// maxWidth: Get.width * .9,
|
||||
// imageQuality: 100,
|
||||
);
|
||||
image = File(pickedImage!.path);
|
||||
|
||||
croppedFile = await ImageCropper().cropImage(
|
||||
sourcePath: image!.path,
|
||||
uiSettings: [
|
||||
AndroidUiSettings(
|
||||
toolbarTitle: 'Cropper'.tr,
|
||||
toolbarColor: AppColor.blueColor,
|
||||
toolbarWidgetColor: AppColor.yellowColor,
|
||||
initAspectRatio: CropAspectRatioPreset.original,
|
||||
lockAspectRatio: false),
|
||||
IOSUiSettings(
|
||||
title: 'Cropper'.tr,
|
||||
),
|
||||
],
|
||||
);
|
||||
myImage = File(pickedImage.path);
|
||||
isloading = true;
|
||||
update();
|
||||
// Save the cropped image
|
||||
// File savedCroppedImage = File(croppedFile!.path);
|
||||
File compressedImage = await compressImage(File(croppedFile!.path));
|
||||
print('link =$link');
|
||||
try {
|
||||
var response = await uploadImage(
|
||||
compressedImage,
|
||||
{'driverID': (box.read(BoxName.driverID)), 'imageType': imageType},
|
||||
link,
|
||||
);
|
||||
|
||||
// Save the returned URL from the V3 backend to local storage
|
||||
if (response != null && response['status'] == 'success' && response['message'] != null) {
|
||||
if (response['message']['file_link'] != null) {
|
||||
box.write(BoxName.driverPhotoUrl, response['message']['file_link'].toString());
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('e: ${e}');
|
||||
mySnackeBarError('Image Upload Failed'.tr);
|
||||
} finally {
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
uploadImagePicture(File file, Map data, String link) async {
|
||||
final String token = r(box.read(BoxName.jwt)).split(AppInformation.addd)[0];
|
||||
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
||||
|
||||
var request = http.MultipartRequest('POST', Uri.parse(link));
|
||||
request.headers.addAll({
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-FP': fingerPrint,
|
||||
});
|
||||
|
||||
var length = await file.length();
|
||||
var stream = http.ByteStream(file.openRead());
|
||||
request.files.add(
|
||||
http.MultipartFile(
|
||||
'image',
|
||||
stream,
|
||||
length,
|
||||
filename: '${box.read(BoxName.driverID)}.jpg',
|
||||
),
|
||||
);
|
||||
data.forEach((key, value) {
|
||||
request.fields[key] = value;
|
||||
});
|
||||
|
||||
var myrequest = await request.send();
|
||||
var res = await http.Response.fromStream(myrequest);
|
||||
Log.print('uploadImagePicture response [${res.statusCode}]: ${res.body}');
|
||||
if (res.statusCode == 200) {
|
||||
return jsonDecode(res.body);
|
||||
} else {
|
||||
throw Exception('Failed to upload image: ${res.statusCode} - ${res.body}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<File> compressImage(File file) async {
|
||||
final dir = await path_provider.getTemporaryDirectory();
|
||||
final targetPath = "${dir.absolute.path}/temp.jpg";
|
||||
|
||||
var result = await FlutterImageCompress.compressAndGetFile(
|
||||
file.absolute.path,
|
||||
targetPath,
|
||||
quality: 70,
|
||||
minWidth: 1024,
|
||||
minHeight: 1024,
|
||||
);
|
||||
|
||||
return File(result!.path);
|
||||
}
|
||||
|
||||
// Future<File> detectAndCropFace(File imageFile) async {
|
||||
// final inputImage = InputImage.fromFilePath(imageFile.path);
|
||||
// final options = FaceDetectorOptions(
|
||||
// enableClassification: false,
|
||||
// enableLandmarks: false,
|
||||
// enableTracking: false,
|
||||
// minFaceSize: 0.15,
|
||||
// performanceMode: FaceDetectorMode.accurate,
|
||||
// );
|
||||
// final faceDetector = FaceDetector(options: options);
|
||||
|
||||
// try {
|
||||
// final List<Face> faces = await faceDetector.processImage(inputImage);
|
||||
// final image = img.decodeImage(await imageFile.readAsBytes());
|
||||
|
||||
// if (image == null) throw Exception('Unable to decode image');
|
||||
|
||||
// int left, top, width, height;
|
||||
|
||||
// if (faces.isNotEmpty) {
|
||||
// // Face detected, crop around the face
|
||||
// final face = faces[0];
|
||||
// double padding = 0.2; // 20% padding
|
||||
// int paddingX = (face.boundingBox.width * padding).round();
|
||||
// int paddingY = (face.boundingBox.height * padding).round();
|
||||
|
||||
// left = (face.boundingBox.left - paddingX).round();
|
||||
// top = (face.boundingBox.top - paddingY).round();
|
||||
// width = (face.boundingBox.width + 2 * paddingX).round();
|
||||
// height = (face.boundingBox.height + 2 * paddingY).round();
|
||||
// } else {
|
||||
// // No face detected, crop the center of the image
|
||||
// int size = min(image.width, image.height);
|
||||
// left = (image.width - size) ~/ 2;
|
||||
// top = (image.height - size) ~/ 2;
|
||||
// width = size;
|
||||
// height = size;
|
||||
// }
|
||||
|
||||
// // Ensure dimensions are within image bounds
|
||||
// left = left.clamp(0, image.width - 1);
|
||||
// top = top.clamp(0, image.height - 1);
|
||||
// width = width.clamp(1, image.width - left);
|
||||
// height = height.clamp(1, image.height - top);
|
||||
|
||||
// final croppedImage =
|
||||
// img.copyCrop(image, x: left, y: top, width: width, height: height);
|
||||
|
||||
// // Save the cropped image
|
||||
// final tempDir = await path_provider.getTemporaryDirectory();
|
||||
// final tempPath = tempDir.path;
|
||||
// final croppedFile = File('$tempPath/cropped_image.jpg');
|
||||
// await croppedFile.writeAsBytes(img.encodeJpg(croppedImage, quality: 100));
|
||||
|
||||
// return croppedFile;
|
||||
// } finally {
|
||||
// faceDetector.close();
|
||||
// }
|
||||
// }
|
||||
15
siro_driver/lib/controller/functions/vibrate.dart
Executable file
15
siro_driver/lib/controller/functions/vibrate.dart
Executable file
@@ -0,0 +1,15 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../main.dart';
|
||||
|
||||
class HomePageController extends GetxController {
|
||||
late bool isVibrate = box.read(BoxName.isvibrate) ?? true;
|
||||
|
||||
void changeVibrateOption(bool value) {
|
||||
isVibrate = box.read(BoxName.isvibrate) ?? true;
|
||||
isVibrate = value;
|
||||
box.write(BoxName.isvibrate, value);
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
// ════════════════════════════════════════════
|
||||
// نموذج التحدي
|
||||
// ════════════════════════════════════════════
|
||||
|
||||
class Challenge {
|
||||
final String id;
|
||||
final String titleEn;
|
||||
final String titleAr;
|
||||
final String descriptionEn;
|
||||
final String descriptionAr;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final int target;
|
||||
final int reward; // نقاط
|
||||
final String type; // 'daily' or 'weekly'
|
||||
final String metric; // 'trips', 'earnings', 'hours', 'acceptance_rate'
|
||||
int currentProgress;
|
||||
bool isClaimed;
|
||||
|
||||
Challenge({
|
||||
required this.id,
|
||||
required this.titleEn,
|
||||
required this.titleAr,
|
||||
required this.descriptionEn,
|
||||
required this.descriptionAr,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.target,
|
||||
required this.reward,
|
||||
required this.type,
|
||||
required this.metric,
|
||||
this.currentProgress = 0,
|
||||
this.isClaimed = false,
|
||||
});
|
||||
|
||||
double get progress => (currentProgress / target).clamp(0.0, 1.0);
|
||||
bool get isCompleted => currentProgress >= target;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════
|
||||
// Controller
|
||||
// ════════════════════════════════════════════
|
||||
|
||||
class ChallengesController extends GetxController {
|
||||
bool isLoading = false;
|
||||
List<Challenge> dailyChallenges = [];
|
||||
List<Challenge> weeklyChallenges = [];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_generateChallenges();
|
||||
_loadProgress();
|
||||
fetchChallengeProgress();
|
||||
}
|
||||
|
||||
void _generateChallenges() {
|
||||
final now = DateTime.now();
|
||||
final isWeekend = now.weekday == 5 || now.weekday == 6; // الجمعة والسبت
|
||||
|
||||
dailyChallenges = [
|
||||
Challenge(
|
||||
id: 'daily_trips_5',
|
||||
titleEn: 'Road Runner',
|
||||
titleAr: 'سائق سريع',
|
||||
descriptionEn: 'Complete 5 trips today',
|
||||
descriptionAr: 'أكمل 5 رحلات اليوم',
|
||||
icon: Icons.local_taxi_rounded,
|
||||
color: const Color(0xFF2196F3),
|
||||
target: 5,
|
||||
reward: 50,
|
||||
type: 'daily',
|
||||
metric: 'trips',
|
||||
),
|
||||
Challenge(
|
||||
id: 'daily_trips_10',
|
||||
titleEn: 'Marathon Driver',
|
||||
titleAr: 'سائق الماراثون',
|
||||
descriptionEn: 'Complete 10 trips today',
|
||||
descriptionAr: 'أكمل 10 رحلات اليوم',
|
||||
icon: Icons.directions_car_rounded,
|
||||
color: const Color(0xFFFF9800),
|
||||
target: 10,
|
||||
reward: 150,
|
||||
type: 'daily',
|
||||
metric: 'trips',
|
||||
),
|
||||
Challenge(
|
||||
id: 'daily_earnings',
|
||||
titleEn: 'Money Maker',
|
||||
titleAr: 'صانع المال',
|
||||
descriptionEn: 'Earn 3000 SYP today',
|
||||
descriptionAr: 'اربح 3000 ل.س اليوم',
|
||||
icon: Icons.monetization_on_rounded,
|
||||
color: const Color(0xFF4CAF50),
|
||||
target: 3000,
|
||||
reward: 100,
|
||||
type: 'daily',
|
||||
metric: 'earnings',
|
||||
),
|
||||
if (isWeekend)
|
||||
Challenge(
|
||||
id: 'daily_weekend_bonus',
|
||||
titleEn: 'Weekend Warrior',
|
||||
titleAr: 'محارب عطلة نهاية الأسبوع',
|
||||
descriptionEn: 'Complete 8 trips on the weekend',
|
||||
descriptionAr: 'أكمل 8 رحلات في عطلة نهاية الأسبوع',
|
||||
icon: Icons.celebration_rounded,
|
||||
color: const Color(0xFFE91E63),
|
||||
target: 8,
|
||||
reward: 200,
|
||||
type: 'daily',
|
||||
metric: 'trips',
|
||||
),
|
||||
];
|
||||
|
||||
weeklyChallenges = [
|
||||
Challenge(
|
||||
id: 'weekly_trips_30',
|
||||
titleEn: 'Weekly Champion',
|
||||
titleAr: 'بطل الأسبوع',
|
||||
descriptionEn: 'Complete 30 trips this week',
|
||||
descriptionAr: 'أكمل 30 رحلة هذا الأسبوع',
|
||||
icon: Icons.emoji_events_rounded,
|
||||
color: const Color(0xFFFFD700),
|
||||
target: 30,
|
||||
reward: 300,
|
||||
type: 'weekly',
|
||||
metric: 'trips',
|
||||
),
|
||||
Challenge(
|
||||
id: 'weekly_earnings',
|
||||
titleEn: 'Big Earner',
|
||||
titleAr: 'الربح الكبير',
|
||||
descriptionEn: 'Earn 20,000 SYP this week',
|
||||
descriptionAr: 'اربح 20,000 ل.س هذا الأسبوع',
|
||||
icon: Icons.account_balance_wallet_rounded,
|
||||
color: const Color(0xFF9C27B0),
|
||||
target: 20000,
|
||||
reward: 500,
|
||||
type: 'weekly',
|
||||
metric: 'earnings',
|
||||
),
|
||||
Challenge(
|
||||
id: 'weekly_hours',
|
||||
titleEn: 'Time Master',
|
||||
titleAr: 'سيد الوقت',
|
||||
descriptionEn: 'Drive for 20 hours this week',
|
||||
descriptionAr: 'اقضِ 20 ساعة في القيادة هذا الأسبوع',
|
||||
icon: Icons.timer_rounded,
|
||||
color: const Color(0xFF00BCD4),
|
||||
target: 20,
|
||||
reward: 400,
|
||||
type: 'weekly',
|
||||
metric: 'hours',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
void _loadProgress() {
|
||||
final today = DateTime.now().toIso8601String().split('T')[0];
|
||||
final savedDate = box.read('challenges_date');
|
||||
|
||||
if (savedDate != today) {
|
||||
// يوم جديد — إعادة تعيين التحديات اليومية
|
||||
box.write('challenges_date', today);
|
||||
for (var c in dailyChallenges) {
|
||||
box.write('challenge_${c.id}_claimed', false);
|
||||
}
|
||||
}
|
||||
|
||||
// تحميل حالة المطالبة
|
||||
for (var c in dailyChallenges) {
|
||||
c.isClaimed = box.read('challenge_${c.id}_claimed') ?? false;
|
||||
}
|
||||
for (var c in weeklyChallenges) {
|
||||
c.isClaimed = box.read('challenge_${c.id}_claimed') ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchChallengeProgress() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
// جلب رحلات اليوم
|
||||
var todayRes = await CRUD().getWallet(
|
||||
link: AppLink.getDriverPaymentToday,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
|
||||
int todayTrips = 0;
|
||||
double todayEarnings = 0;
|
||||
|
||||
if (todayRes != null && todayRes != 'failure') {
|
||||
var data = jsonDecode(todayRes);
|
||||
todayEarnings = double.tryParse(data['message']?[0]?['todayAmount']?.toString() ?? '0') ?? 0;
|
||||
todayTrips = int.tryParse(data['message']?[0]?['todayCount']?.toString() ?? '0') ?? 0;
|
||||
}
|
||||
|
||||
// تحديث التحديات اليومية
|
||||
for (var c in dailyChallenges) {
|
||||
switch (c.metric) {
|
||||
case 'trips':
|
||||
c.currentProgress = todayTrips;
|
||||
break;
|
||||
case 'earnings':
|
||||
c.currentProgress = todayEarnings.toInt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fetch weekly earnings from PAYMENT server
|
||||
var weeklyEarningsRes = await CRUD().getWallet(
|
||||
link: AppLink.getDriverWeekPaymentMove,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
|
||||
double weeklyEarnings = 0;
|
||||
if (weeklyEarningsRes != null && weeklyEarningsRes != 'failure') {
|
||||
var data = jsonDecode(weeklyEarningsRes);
|
||||
if (data['message'] is List && data['message'].isNotEmpty) {
|
||||
weeklyEarnings = double.tryParse(data['message'][0]['totalAmount']?.toString() ?? '0') ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fetch weekly trips and hours from RIDES server (avoiding earnings join)
|
||||
var weeklyAggregateRes = await CRUD().get(
|
||||
link: AppLink.getWeeklyAggregate,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
|
||||
int weeklyTrips = 0;
|
||||
double weeklyHours = 0;
|
||||
|
||||
if (weeklyAggregateRes != null && weeklyAggregateRes != 'failure') {
|
||||
var data = jsonDecode(weeklyAggregateRes);
|
||||
if (data['message'] is List) {
|
||||
for (var day in data['message']) {
|
||||
weeklyTrips += int.tryParse(day['trips']?.toString() ?? '0') ?? 0;
|
||||
weeklyHours += double.tryParse(day['hours']?.toString() ?? '0') ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var c in weeklyChallenges) {
|
||||
switch (c.metric) {
|
||||
case 'trips':
|
||||
c.currentProgress = weeklyTrips;
|
||||
break;
|
||||
case 'earnings':
|
||||
c.currentProgress = weeklyEarnings.toInt();
|
||||
break;
|
||||
case 'hours':
|
||||
c.currentProgress = weeklyHours.toInt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Challenges] Error: $e');
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> claimReward(Challenge challenge) async {
|
||||
if (!challenge.isCompleted || challenge.isClaimed) return;
|
||||
|
||||
try {
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.claimChallengeReward,
|
||||
payload: {
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
'challenge_id': challenge.id,
|
||||
'points': challenge.reward.toString(),
|
||||
},
|
||||
);
|
||||
|
||||
if (res != null && res != 'failure') {
|
||||
challenge.isClaimed = true;
|
||||
box.write('challenge_${challenge.id}_claimed', true);
|
||||
debugPrint('🎉 Claimed ${challenge.reward} points for ${challenge.id}');
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Challenges] Claim error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import '../../main.dart';
|
||||
|
||||
// ════════════════════════════════════════════
|
||||
// نماذج البيانات
|
||||
// ════════════════════════════════════════════
|
||||
|
||||
class DriverLevel {
|
||||
final String id;
|
||||
final String nameEn;
|
||||
final String nameAr;
|
||||
final String emoji;
|
||||
final Color color;
|
||||
final Color gradientEnd;
|
||||
final int minPoints;
|
||||
final int maxPoints;
|
||||
final double commissionDiscount; // نسبة الخصم على العمولة
|
||||
final List<String> perks;
|
||||
|
||||
const DriverLevel({
|
||||
required this.id,
|
||||
required this.nameEn,
|
||||
required this.nameAr,
|
||||
required this.emoji,
|
||||
required this.color,
|
||||
required this.gradientEnd,
|
||||
required this.minPoints,
|
||||
required this.maxPoints,
|
||||
required this.commissionDiscount,
|
||||
required this.perks,
|
||||
});
|
||||
}
|
||||
|
||||
class Achievement {
|
||||
final String id;
|
||||
final String titleEn;
|
||||
final String titleAr;
|
||||
final String descriptionEn;
|
||||
final String descriptionAr;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final int target;
|
||||
final String type; // 'trips', 'rating', 'earnings', 'streak', 'referral'
|
||||
bool isUnlocked;
|
||||
int currentProgress;
|
||||
final DateTime? unlockedAt;
|
||||
|
||||
Achievement({
|
||||
required this.id,
|
||||
required this.titleEn,
|
||||
required this.titleAr,
|
||||
required this.descriptionEn,
|
||||
required this.descriptionAr,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.target,
|
||||
required this.type,
|
||||
this.isUnlocked = false,
|
||||
this.currentProgress = 0,
|
||||
this.unlockedAt,
|
||||
});
|
||||
|
||||
double get progress => (currentProgress / target).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════
|
||||
// المستويات الثابتة
|
||||
// ════════════════════════════════════════════
|
||||
|
||||
class DriverLevels {
|
||||
static const List<DriverLevel> all = [
|
||||
DriverLevel(
|
||||
id: 'bronze',
|
||||
nameEn: 'Bronze',
|
||||
nameAr: 'برونزي',
|
||||
emoji: '🥉',
|
||||
color: Color(0xFFCD7F32),
|
||||
gradientEnd: Color(0xFFE8A854),
|
||||
minPoints: 0,
|
||||
maxPoints: 999,
|
||||
commissionDiscount: 0,
|
||||
perks: ['Basic features', 'Standard support'],
|
||||
),
|
||||
DriverLevel(
|
||||
id: 'silver',
|
||||
nameEn: 'Silver',
|
||||
nameAr: 'فضي',
|
||||
emoji: '🥈',
|
||||
color: Color(0xFF9CA3AF),
|
||||
gradientEnd: Color(0xFFC0C7D1),
|
||||
minPoints: 1000,
|
||||
maxPoints: 4999,
|
||||
commissionDiscount: 1,
|
||||
perks: ['Priority medium', 'Silver badge', '-1% commission'],
|
||||
),
|
||||
DriverLevel(
|
||||
id: 'gold',
|
||||
nameEn: 'Gold',
|
||||
nameAr: 'ذهبي',
|
||||
emoji: '🥇',
|
||||
color: Color(0xFFFFD700),
|
||||
gradientEnd: Color(0xFFFFA500),
|
||||
minPoints: 5000,
|
||||
maxPoints: 14999,
|
||||
commissionDiscount: 2,
|
||||
perks: ['High priority', 'Gold badge', '-2% commission'],
|
||||
),
|
||||
DriverLevel(
|
||||
id: 'diamond',
|
||||
nameEn: 'Diamond',
|
||||
nameAr: 'ألماسي',
|
||||
emoji: '💎',
|
||||
color: Color(0xFF00BCD4),
|
||||
gradientEnd: Color(0xFF3F51B5),
|
||||
minPoints: 15000,
|
||||
maxPoints: 999999,
|
||||
commissionDiscount: 5,
|
||||
perks: ['VIP first', 'Diamond badge', '-5% commission', 'Priority support'],
|
||||
),
|
||||
];
|
||||
|
||||
static DriverLevel getLevel(int points) {
|
||||
for (int i = all.length - 1; i >= 0; i--) {
|
||||
if (points >= all[i].minPoints) return all[i];
|
||||
}
|
||||
return all.first;
|
||||
}
|
||||
|
||||
static DriverLevel? getNextLevel(int points) {
|
||||
final current = getLevel(points);
|
||||
final idx = all.indexOf(current);
|
||||
if (idx < all.length - 1) return all[idx + 1];
|
||||
return null;
|
||||
}
|
||||
|
||||
static double getProgressToNext(int points) {
|
||||
final current = getLevel(points);
|
||||
final next = getNextLevel(points);
|
||||
if (next == null) return 1.0;
|
||||
return ((points - current.minPoints) / (next.minPoints - current.minPoints))
|
||||
.clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════
|
||||
// الـ Controller
|
||||
// ════════════════════════════════════════════
|
||||
|
||||
class GamificationController extends GetxController {
|
||||
bool isLoading = false;
|
||||
int totalTrips = 0;
|
||||
int totalPoints = 0;
|
||||
double averageRating = 5.0;
|
||||
int totalReferrals = 0;
|
||||
int consecutiveDays = 0; // أيام متتالية
|
||||
double totalEarnings = 0;
|
||||
|
||||
// === Driving Behavior ===
|
||||
double behaviorScore = 100.0;
|
||||
int hardBrakes = 0;
|
||||
double maxSpeed = 0.0;
|
||||
|
||||
late DriverLevel currentLevel;
|
||||
DriverLevel? nextLevel;
|
||||
double progressToNext = 0;
|
||||
List<Achievement> achievements = [];
|
||||
|
||||
// === Daily Goal ===
|
||||
double dailyGoal = 0;
|
||||
double dailyEarnings = 0;
|
||||
double get dailyGoalProgress =>
|
||||
dailyGoal > 0 ? (dailyEarnings / dailyGoal).clamp(0.0, 1.0) : 0.0;
|
||||
bool get isDailyGoalMet => dailyGoalProgress >= 1.0;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_loadLocalData();
|
||||
_initializeAchievements();
|
||||
_calculateLevel();
|
||||
fetchGamificationData();
|
||||
}
|
||||
|
||||
// ═══════ تحميل البيانات المحلية ═══════
|
||||
void _loadLocalData() {
|
||||
dailyGoal = (box.read('dailyGoal') ?? 0).toDouble();
|
||||
totalTrips = box.read('gamification_totalTrips') ?? 0;
|
||||
consecutiveDays = box.read('gamification_consecutiveDays') ?? 0;
|
||||
}
|
||||
|
||||
// ═══════ حفظ الهدف اليومي ═══════
|
||||
void setDailyGoal(double goal) {
|
||||
dailyGoal = goal;
|
||||
box.write('dailyGoal', goal);
|
||||
update();
|
||||
}
|
||||
|
||||
void updateDailyEarnings(double earnings) {
|
||||
dailyEarnings = earnings;
|
||||
update();
|
||||
}
|
||||
|
||||
// ═══════ حساب المستوى ═══════
|
||||
void _calculateLevel() {
|
||||
currentLevel = DriverLevels.getLevel(totalPoints);
|
||||
nextLevel = DriverLevels.getNextLevel(totalPoints);
|
||||
progressToNext = DriverLevels.getProgressToNext(totalPoints);
|
||||
update();
|
||||
}
|
||||
|
||||
// ═══════ تهيئة الإنجازات ═══════
|
||||
void _initializeAchievements() {
|
||||
achievements = [
|
||||
Achievement(
|
||||
id: 'first_trip',
|
||||
titleEn: 'First Trip',
|
||||
titleAr: 'أول رحلة',
|
||||
descriptionEn: 'Complete your first trip',
|
||||
descriptionAr: 'أكمل أول رحلة لك',
|
||||
icon: Icons.flag_rounded,
|
||||
color: const Color(0xFF4CAF50),
|
||||
target: 1,
|
||||
type: 'trips',
|
||||
),
|
||||
Achievement(
|
||||
id: 'trip_50',
|
||||
titleEn: 'Road Warrior',
|
||||
titleAr: 'محارب الطريق',
|
||||
descriptionEn: 'Complete 50 trips',
|
||||
descriptionAr: 'أكمل 50 رحلة',
|
||||
icon: Icons.local_taxi_rounded,
|
||||
color: const Color(0xFF2196F3),
|
||||
target: 50,
|
||||
type: 'trips',
|
||||
),
|
||||
Achievement(
|
||||
id: 'trip_100',
|
||||
titleEn: 'Century Rider',
|
||||
titleAr: 'سائق المئة',
|
||||
descriptionEn: 'Complete 100 trips',
|
||||
descriptionAr: 'أكمل 100 رحلة',
|
||||
icon: Icons.emoji_events_rounded,
|
||||
color: const Color(0xFFFF9800),
|
||||
target: 100,
|
||||
type: 'trips',
|
||||
),
|
||||
Achievement(
|
||||
id: 'trip_500',
|
||||
titleEn: 'Road Legend',
|
||||
titleAr: 'أسطورة الطريق',
|
||||
descriptionEn: 'Complete 500 trips',
|
||||
descriptionAr: 'أكمل 500 رحلة',
|
||||
icon: Icons.stars_rounded,
|
||||
color: const Color(0xFFE91E63),
|
||||
target: 500,
|
||||
type: 'trips',
|
||||
),
|
||||
Achievement(
|
||||
id: 'five_star',
|
||||
titleEn: 'Five Star Driver',
|
||||
titleAr: 'سائق 5 نجوم',
|
||||
descriptionEn: 'Maintain 5.0 rating',
|
||||
descriptionAr: 'حافظ على تقييم 5.0',
|
||||
icon: Icons.star_rounded,
|
||||
color: const Color(0xFFFFD700),
|
||||
target: 5,
|
||||
type: 'rating',
|
||||
),
|
||||
Achievement(
|
||||
id: 'streak_7',
|
||||
titleEn: 'Weekly Streak',
|
||||
titleAr: 'سلسلة أسبوعية',
|
||||
descriptionEn: 'Work 7 consecutive days',
|
||||
descriptionAr: 'اعمل 7 أيام متتالية',
|
||||
icon: Icons.whatshot_rounded,
|
||||
color: const Color(0xFFFF5722),
|
||||
target: 7,
|
||||
type: 'streak',
|
||||
),
|
||||
Achievement(
|
||||
id: 'streak_30',
|
||||
titleEn: 'Monthly Streak',
|
||||
titleAr: 'سلسلة شهرية',
|
||||
descriptionEn: 'Work 30 consecutive days',
|
||||
descriptionAr: 'اعمل 30 يوم متتالي',
|
||||
icon: Icons.local_fire_department_rounded,
|
||||
color: const Color(0xFFD32F2F),
|
||||
target: 30,
|
||||
type: 'streak',
|
||||
),
|
||||
Achievement(
|
||||
id: 'referral_5',
|
||||
titleEn: 'Social Butterfly',
|
||||
titleAr: 'الفراشة الاجتماعية',
|
||||
descriptionEn: 'Refer 5 drivers',
|
||||
descriptionAr: 'ادعُ 5 سائقين',
|
||||
icon: Icons.people_rounded,
|
||||
color: const Color(0xFF9C27B0),
|
||||
target: 5,
|
||||
type: 'referral',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// ═══════ تحديث تقدم الإنجازات ═══════
|
||||
void _updateAchievementProgress() {
|
||||
for (var ach in achievements) {
|
||||
switch (ach.type) {
|
||||
case 'trips':
|
||||
ach.currentProgress = totalTrips;
|
||||
break;
|
||||
case 'rating':
|
||||
ach.currentProgress = averageRating >= 5 ? 5 : averageRating.floor();
|
||||
break;
|
||||
case 'streak':
|
||||
ach.currentProgress = consecutiveDays;
|
||||
break;
|
||||
case 'referral':
|
||||
ach.currentProgress = totalReferrals;
|
||||
break;
|
||||
}
|
||||
ach.isUnlocked = ach.currentProgress >= ach.target;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
// ═══════ جلب البيانات من السيرفر ═══════
|
||||
Future<void> fetchGamificationData() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
// 1. جلب عدد الرحلات الكلي
|
||||
var tripRes = await CRUD().get(
|
||||
link: AppLink.getTripCountByCaptain,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (tripRes != null && tripRes != 'failure') {
|
||||
var data = jsonDecode(tripRes);
|
||||
totalTrips =
|
||||
int.tryParse(data['message']?[0]?['count']?.toString() ?? '0') ?? 0;
|
||||
box.write('gamification_totalTrips', totalTrips);
|
||||
}
|
||||
|
||||
// 2. جلب التقييم
|
||||
var rateRes = await CRUD().get(
|
||||
link: AppLink.getDriverRate,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (rateRes != null && rateRes != 'failure') {
|
||||
var data = jsonDecode(rateRes);
|
||||
averageRating =
|
||||
double.tryParse(data['message']?[0]?['rating']?.toString() ?? '5') ??
|
||||
5.0;
|
||||
}
|
||||
|
||||
// 3. جلب النقاط (الرصيد)
|
||||
var pointsRes = await CRUD().getWallet(
|
||||
link: AppLink.getDriverPaymentPoints,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (pointsRes != null && pointsRes != 'failure') {
|
||||
var data = jsonDecode(pointsRes);
|
||||
totalPoints = double.tryParse(
|
||||
data['message']?[0]?['total_amount']?.toString() ?? '0')
|
||||
?.abs()
|
||||
.toInt() ??
|
||||
0;
|
||||
}
|
||||
|
||||
// 4. جلب عدد الدعوات
|
||||
var invRes = await CRUD().get(
|
||||
link: AppLink.getInviteDriver,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (invRes != null && invRes != 'failure') {
|
||||
var data = jsonDecode(invRes);
|
||||
if (data['message'] is List) {
|
||||
totalReferrals = (data['message'] as List).length;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. جلب أرباح اليوم
|
||||
var todayRes = await CRUD().getWallet(
|
||||
link: AppLink.getDriverPaymentToday,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (todayRes != null && todayRes != 'failure') {
|
||||
var data = jsonDecode(todayRes);
|
||||
dailyEarnings = double.tryParse(
|
||||
data['message']?[0]?['todayAmount']?.toString() ?? '0') ??
|
||||
0;
|
||||
}
|
||||
|
||||
// 6. جلب تقييم سلوك القيادة
|
||||
var behaviorRes = await CRUD().get(
|
||||
link: AppLink.getDriverBehavior,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (behaviorRes != null && behaviorRes != 'failure') {
|
||||
var data = jsonDecode(behaviorRes);
|
||||
if (data['message'] is List && data['message'].isNotEmpty) {
|
||||
var behavior = data['message'][0];
|
||||
behaviorScore = double.tryParse(behavior['avg_score']?.toString() ?? '100') ?? 100.0;
|
||||
hardBrakes = int.tryParse(behavior['total_hard_brakes']?.toString() ?? '0') ?? 0;
|
||||
maxSpeed = double.tryParse(behavior['max_speed']?.toString() ?? '0') ?? 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. حساب الأيام المتتالية (محلياً)
|
||||
_calculateConsecutiveDays();
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Gamification] Error fetching data: $e');
|
||||
}
|
||||
|
||||
_calculateLevel();
|
||||
_updateAchievementProgress();
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
void _calculateConsecutiveDays() {
|
||||
String? lastActiveDate = box.read('lastActiveDate');
|
||||
String today =
|
||||
DateTime.now().toIso8601String().split('T')[0]; // 2026-05-08
|
||||
|
||||
if (lastActiveDate == null) {
|
||||
consecutiveDays = 1;
|
||||
} else if (lastActiveDate == today) {
|
||||
// نفس اليوم — لا تغيير
|
||||
} else {
|
||||
DateTime last = DateTime.parse(lastActiveDate);
|
||||
DateTime now = DateTime.parse(today);
|
||||
if (now.difference(last).inDays == 1) {
|
||||
consecutiveDays++;
|
||||
} else {
|
||||
consecutiveDays = 1;
|
||||
}
|
||||
}
|
||||
|
||||
box.write('lastActiveDate', today);
|
||||
box.write('gamification_consecutiveDays', consecutiveDays);
|
||||
}
|
||||
|
||||
// ═══════ إحصائيات سريعة ═══════
|
||||
int get unlockedCount => achievements.where((a) => a.isUnlocked).length;
|
||||
int get totalAchievements => achievements.length;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import '../../main.dart';
|
||||
|
||||
class LeaderboardEntry {
|
||||
final String driverId;
|
||||
final String name;
|
||||
final String photoUrl;
|
||||
final int rank;
|
||||
final double value; // trips or earnings
|
||||
final bool isCurrentUser;
|
||||
|
||||
LeaderboardEntry(
|
||||
{required this.driverId,
|
||||
required this.name,
|
||||
required this.photoUrl,
|
||||
required this.rank,
|
||||
required this.value,
|
||||
this.isCurrentUser = false});
|
||||
}
|
||||
|
||||
class LeaderboardController extends GetxController {
|
||||
bool isLoading = false;
|
||||
int selectedTab = 0; // 0=trips, 1=earnings
|
||||
List<LeaderboardEntry> tripLeaderboard = [];
|
||||
List<LeaderboardEntry> earningsLeaderboard = [];
|
||||
int myRank = 0;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
fetchLeaderboard();
|
||||
}
|
||||
|
||||
void changeTab(int tab) {
|
||||
selectedTab = tab;
|
||||
update();
|
||||
}
|
||||
|
||||
List<LeaderboardEntry> get currentLeaderboard =>
|
||||
selectedTab == 0 ? tripLeaderboard : earningsLeaderboard;
|
||||
Future<void> fetchLeaderboard() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
final myId = box.read(BoxName.driverID)?.toString() ?? '';
|
||||
|
||||
// Fetch trips leaderboard
|
||||
var resTrips = await CRUD().post(
|
||||
link: AppLink.getLeaderboard,
|
||||
payload: {'type': 'trips'},
|
||||
);
|
||||
if (resTrips != null && resTrips != 'failure') {
|
||||
var data = jsonDecode(resTrips);
|
||||
if (data['message'] is List) {
|
||||
tripLeaderboard = (data['message'] as List).map((e) => LeaderboardEntry(
|
||||
driverId: e['driver_id'].toString(),
|
||||
name: e['name'].toString(),
|
||||
photoUrl: e['photoUrl']?.toString() ?? '',
|
||||
rank: int.tryParse(e['rank']?.toString() ?? '0') ?? 0,
|
||||
value: double.tryParse(e['value']?.toString() ?? '0') ?? 0,
|
||||
isCurrentUser: e['driver_id'].toString() == myId,
|
||||
)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch earnings leaderboard
|
||||
var resEarnings = await CRUD().post(
|
||||
link: AppLink.getLeaderboard,
|
||||
payload: {'type': 'earnings'},
|
||||
);
|
||||
if (resEarnings != null && resEarnings != 'failure') {
|
||||
var data = jsonDecode(resEarnings);
|
||||
if (data['message'] is List) {
|
||||
earningsLeaderboard = (data['message'] as List).map((e) => LeaderboardEntry(
|
||||
driverId: e['driver_id'].toString(),
|
||||
name: e['name'].toString(),
|
||||
photoUrl: e['photoUrl']?.toString() ?? '',
|
||||
rank: int.tryParse(e['rank']?.toString() ?? '0') ?? 0,
|
||||
value: double.tryParse(e['value']?.toString() ?? '0') ?? 0,
|
||||
isCurrentUser: e['driver_id'].toString() == myId,
|
||||
)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Find my rank
|
||||
final myTripEntry = tripLeaderboard.firstWhereOrNull((e) => e.isCurrentUser);
|
||||
final myEarnEntry = earningsLeaderboard.firstWhereOrNull((e) => e.isCurrentUser);
|
||||
myRank = selectedTab == 0 ? (myTripEntry?.rank ?? 0) : (myEarnEntry?.rank ?? 0);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Leaderboard] Error: $e');
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
189
siro_driver/lib/controller/gamification/referral_controller.dart
Normal file
189
siro_driver/lib/controller/gamification/referral_controller.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import '../../main.dart';
|
||||
|
||||
// ════════════════════════════════════════════
|
||||
// نموذج الإحالة
|
||||
// ════════════════════════════════════════════
|
||||
|
||||
class ReferralRecord {
|
||||
final String id;
|
||||
final String name;
|
||||
final String phone;
|
||||
final String status; // 'registered', 'active', 'inactive'
|
||||
final String type; // 'driver', 'passenger'
|
||||
final String joinDate;
|
||||
final int tripCount;
|
||||
|
||||
ReferralRecord({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.phone,
|
||||
required this.status,
|
||||
required this.type,
|
||||
required this.joinDate,
|
||||
required this.tripCount,
|
||||
});
|
||||
|
||||
factory ReferralRecord.fromJson(Map<String, dynamic> json) {
|
||||
return ReferralRecord(
|
||||
id: json['id']?.toString() ?? '',
|
||||
name: json['name']?.toString() ?? json['nameArabic']?.toString() ?? '',
|
||||
phone: json['phone']?.toString() ?? '',
|
||||
status: json['status']?.toString() ?? 'registered',
|
||||
type: json['type']?.toString() ?? 'driver',
|
||||
joinDate: json['created_at']?.toString() ?? '',
|
||||
tripCount: int.tryParse(json['trip_count']?.toString() ?? '0') ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════
|
||||
// Controller
|
||||
// ════════════════════════════════════════════
|
||||
|
||||
class ReferralController extends GetxController {
|
||||
bool isLoading = false;
|
||||
List<ReferralRecord> driverReferrals = [];
|
||||
List<ReferralRecord> passengerReferrals = [];
|
||||
String referralCode = '';
|
||||
int totalDriverReferrals = 0;
|
||||
int totalPassengerReferrals = 0;
|
||||
int activeReferrals = 0;
|
||||
double totalRewardsEarned = 0;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_generateReferralCode();
|
||||
fetchReferralData();
|
||||
}
|
||||
|
||||
void _generateReferralCode() {
|
||||
final driverId = box.read(BoxName.driverID)?.toString() ?? '';
|
||||
final name = box.read(BoxName.nameDriver)?.toString() ?? '';
|
||||
if (driverId.isNotEmpty) {
|
||||
// كود فريد: أول 3 حروف من الاسم + ID
|
||||
final prefix = name.length >= 3 ? name.substring(0, 3).toUpperCase() : name.toUpperCase();
|
||||
referralCode = '$prefix$driverId';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fetchReferralData() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
// 1. جلب دعوات السائقين
|
||||
var driverRes = await CRUD().get(
|
||||
link: AppLink.getInviteDriver,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (driverRes != null && driverRes != 'failure') {
|
||||
var data = jsonDecode(driverRes);
|
||||
if (data['message'] is List) {
|
||||
driverReferrals = (data['message'] as List)
|
||||
.map((e) => ReferralRecord.fromJson(e))
|
||||
.toList();
|
||||
totalDriverReferrals = driverReferrals.length;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. جلب دعوات الركاب
|
||||
var passengerRes = await CRUD().get(
|
||||
link: AppLink.getDriverInvitationToPassengers,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (passengerRes != null && passengerRes != 'failure') {
|
||||
var data = jsonDecode(passengerRes);
|
||||
if (data['message'] is List) {
|
||||
passengerReferrals = (data['message'] as List)
|
||||
.map((e) => ReferralRecord.fromJson(e))
|
||||
.toList();
|
||||
totalPassengerReferrals = passengerReferrals.length;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. جلب الإحصائيات الدقيقة للمكافآت
|
||||
var statsRes = await CRUD().get(
|
||||
link: AppLink.getReferralStats,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (statsRes != null && statsRes != 'failure') {
|
||||
var data = jsonDecode(statsRes);
|
||||
if (data['message'] is List && data['message'].isNotEmpty) {
|
||||
var stats = data['message'][0];
|
||||
totalRewardsEarned = double.tryParse(stats['totalRewards']?.toString() ?? '0') ?? 0;
|
||||
activeReferrals = (int.tryParse(stats['driverInvites']?.toString() ?? '0') ?? 0) +
|
||||
(int.tryParse(stats['passengerInvites']?.toString() ?? '0') ?? 0);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Referral] Error: $e');
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
void copyCode() {
|
||||
Clipboard.setData(ClipboardData(text: referralCode));
|
||||
}
|
||||
|
||||
String get shareMessage {
|
||||
final appName = 'Intaleq';
|
||||
return 'Join $appName as a driver! Use my code: $referralCode\nDownload: https://intaleq.app/driver?ref=$referralCode';
|
||||
}
|
||||
|
||||
String get shareMessagePassenger {
|
||||
final appName = 'Intaleq';
|
||||
return 'Get a ride with $appName! Use my code: $referralCode for a discount.\nDownload: https://intaleq.app?ref=$referralCode';
|
||||
}
|
||||
|
||||
int get totalReferrals => totalDriverReferrals + totalPassengerReferrals;
|
||||
|
||||
// ═══════ إرسال دعوة سائق ═══════
|
||||
Future<bool> inviteDriver(String phone) async {
|
||||
try {
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.addInviteDriver,
|
||||
payload: {
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
'phone': phone,
|
||||
},
|
||||
);
|
||||
if (res != null && res != 'failure') {
|
||||
await fetchReferralData();
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Referral] Invite driver error: $e');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ═══════ إرسال دعوة راكب ═══════
|
||||
Future<bool> invitePassenger(String phone) async {
|
||||
try {
|
||||
var res = await CRUD().post(
|
||||
link: AppLink.addInvitationPassenger,
|
||||
payload: {
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
'phone': phone,
|
||||
},
|
||||
);
|
||||
if (res != null && res != 'failure') {
|
||||
await fetchReferralData();
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Referral] Invite passenger error: $e');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
146
siro_driver/lib/controller/home/captin/behavior_controller.dart
Normal file
146
siro_driver/lib/controller/home/captin/behavior_controller.dart
Normal file
@@ -0,0 +1,146 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
|
||||
import '../../../constant/table_names.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
|
||||
class DriverBehaviorController extends GetxController {
|
||||
Future<List<Map<String, dynamic>>> getAllData() async {
|
||||
return await sql.getAllData(TableName.behavior);
|
||||
}
|
||||
|
||||
var isLoading = false.obs;
|
||||
var overallScore = 100.0.obs;
|
||||
var lastTrips = [].obs;
|
||||
|
||||
Future<void> fetchDriverBehavior() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
final response = await CRUD().get(
|
||||
link: AppLink.get_driver_behavior,
|
||||
payload: {"driver_id": box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
|
||||
if (response != 'failure') {
|
||||
final json = jsonDecode(response);
|
||||
|
||||
overallScore.value =
|
||||
double.parse(json['message']['overall_behavior_score'].toString());
|
||||
lastTrips.value = json['message']['last_10_trips'];
|
||||
} else {
|
||||
// Get.snackbar("Error", json['message'] ?? "Unknown error");
|
||||
}
|
||||
} catch (e) {
|
||||
// Get.snackbar("Error", "Exception: $e");
|
||||
Log.print('e: ${e}');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> analyzeData() async {
|
||||
final data = await getAllData();
|
||||
if (data.isEmpty) return {};
|
||||
|
||||
double maxSpeed = 0;
|
||||
double totalSpeed = 0;
|
||||
int hardBrakes = 0;
|
||||
double totalDistance = 0;
|
||||
|
||||
// متغيرات للمقارنة مع النقطة السابقة
|
||||
double? prevLat, prevLng;
|
||||
DateTime? prevTime;
|
||||
|
||||
// ترتيب البيانات حسب الوقت لضمان دقة الحساب (اختياري لكن مفضل)
|
||||
// data.sort((a, b) => a['created_at'].compareTo(b['created_at']));
|
||||
|
||||
for (var item in data) {
|
||||
// 1. قراءة البيانات بالأسماء الصحيحة من الجدول
|
||||
double lat = item['latitude'] ?? item['lat'] ?? 0.0;
|
||||
double lng = item['longitude'] ?? item['lng'] ?? 0.0;
|
||||
double acc = item['acceleration'] ?? 0.0;
|
||||
|
||||
// قراءة الوقت لحساب السرعة
|
||||
DateTime currentTime =
|
||||
DateTime.tryParse(item['created_at'].toString()) ?? DateTime.now();
|
||||
|
||||
double currentSpeed = 0;
|
||||
|
||||
// 2. حساب السرعة والمسافة إذا وجدت نقطة سابقة
|
||||
if (prevLat != null && prevLng != null && prevTime != null) {
|
||||
double distKm = _calculateDistance(prevLat, prevLng, lat, lng);
|
||||
int timeDiffSeconds = currentTime.difference(prevTime).inSeconds;
|
||||
|
||||
if (timeDiffSeconds > 0) {
|
||||
// السرعة (كم/س) = (المسافة بالكيلومتر * 3600) / الزمن بالثواني
|
||||
currentSpeed = (distKm * 3600) / timeDiffSeconds;
|
||||
}
|
||||
|
||||
totalDistance += distKm;
|
||||
}
|
||||
|
||||
// تحديث القيم الإحصائية
|
||||
if (currentSpeed > maxSpeed) maxSpeed = currentSpeed;
|
||||
totalSpeed += currentSpeed;
|
||||
|
||||
// حساب الفرملة القوية (يعتمد على التسارع المحفوظ مسبقاً)
|
||||
if (acc.abs() > 3.0) hardBrakes++;
|
||||
|
||||
// حفظ النقطة الحالية لتكون هي "السابقة" في الدورة التالية
|
||||
prevLat = lat;
|
||||
prevLng = lng;
|
||||
prevTime = currentTime;
|
||||
}
|
||||
|
||||
// تجنب القسمة على صفر
|
||||
double avgSpeed = (data.length > 1) ? totalSpeed / (data.length - 1) : 0;
|
||||
|
||||
// حساب تقييم السلوك
|
||||
double behaviorScore = 100 - (hardBrakes * 5) - ((maxSpeed > 100) ? 10 : 0);
|
||||
behaviorScore = behaviorScore.clamp(0.0, 100.0);
|
||||
|
||||
return {
|
||||
'max_speed': maxSpeed,
|
||||
'avg_speed': avgSpeed,
|
||||
'hard_brakes': hardBrakes,
|
||||
'total_distance': totalDistance,
|
||||
'behavior_score': behaviorScore,
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> sendSummaryToServer(String driverId, String tripId) async {
|
||||
final summary = await analyzeData();
|
||||
if (summary.isEmpty) return;
|
||||
|
||||
final Map<String, dynamic> body = {
|
||||
'driver_id': driverId,
|
||||
'trip_id': tripId,
|
||||
...summary, // فيه doubles
|
||||
};
|
||||
|
||||
// اجبر كل القيم على String
|
||||
final payload = body.map((k, v) => MapEntry(k, v?.toString() ?? ''));
|
||||
|
||||
await CRUD().post(link: AppLink.saveBehavior, payload: payload);
|
||||
await clearData();
|
||||
}
|
||||
|
||||
Future<void> clearData() async {
|
||||
await sql.deleteAllData(TableName.behavior);
|
||||
}
|
||||
|
||||
double _calculateDistance(
|
||||
double lat1, double lon1, double lat2, double lon2) {
|
||||
const p = 0.017453292519943295;
|
||||
final a = 0.5 -
|
||||
cos((lat2 - lat1) * p) / 2 +
|
||||
cos(lat1 * p) * cos(lat2 * p) * (1 - cos((lon2 - lon1) * p)) / 2;
|
||||
return 12742 * asin(sqrt(a)); // distance in km
|
||||
}
|
||||
}
|
||||
78
siro_driver/lib/controller/home/captin/contact_us_controller.dart
Executable file
78
siro_driver/lib/controller/home/captin/contact_us_controller.dart
Executable file
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../functions/launch.dart';
|
||||
|
||||
class ContactUsController extends GetxController {
|
||||
final String phone1 = '+963992952235';
|
||||
final String phone2 = '+963992952235';
|
||||
final TimeOfDay workStartTime = const TimeOfDay(hour: 12, minute: 0);
|
||||
final TimeOfDay workEndTime = const TimeOfDay(hour: 19, minute: 0);
|
||||
|
||||
bool _isWithinWorkTime(TimeOfDay now) {
|
||||
return (now.hour > workStartTime.hour ||
|
||||
(now.hour == workStartTime.hour &&
|
||||
now.minute >= workStartTime.minute)) &&
|
||||
(now.hour < workEndTime.hour ||
|
||||
(now.hour == workEndTime.hour && now.minute <= workEndTime.minute));
|
||||
}
|
||||
|
||||
void showContactDialog(BuildContext context) {
|
||||
TimeOfDay now = TimeOfDay.now();
|
||||
|
||||
showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (context) => CupertinoActionSheet(
|
||||
title: Text('Contact Us'.tr),
|
||||
message: Text('Choose a contact option'.tr),
|
||||
actions: <Widget>[
|
||||
if (_isWithinWorkTime(now))
|
||||
CupertinoActionSheetAction(
|
||||
child: Text(phone1),
|
||||
onPressed: () => makePhoneCall(
|
||||
phone1,
|
||||
),
|
||||
),
|
||||
if (_isWithinWorkTime(now))
|
||||
CupertinoActionSheetAction(
|
||||
child: Text(phone2),
|
||||
onPressed: () => makePhoneCall(phone2),
|
||||
),
|
||||
if (!_isWithinWorkTime(now))
|
||||
CupertinoActionSheetAction(
|
||||
child: Text(
|
||||
'Work time is from 10:00 - 17:00.\nYou can send a WhatsApp message or email.'
|
||||
.tr),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
const Icon(
|
||||
FontAwesome.whatsapp,
|
||||
color: AppColor.greenColor,
|
||||
),
|
||||
Text('Send WhatsApp Message'.tr),
|
||||
],
|
||||
),
|
||||
onPressed: () =>
|
||||
launchCommunication('whatsapp', phone1, 'Hello'.tr),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
child: Text('Send Email'.tr),
|
||||
onPressed: () =>
|
||||
launchCommunication('email', 'support@sefer.live', 'Hello'.tr),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
child: Text('Cancel'.tr),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
167
siro_driver/lib/controller/home/captin/duration_controller .dart
Executable file
167
siro_driver/lib/controller/home/captin/duration_controller .dart
Executable file
@@ -0,0 +1,167 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/models/model/driver/rides_summary_model.dart';
|
||||
|
||||
import '../../../views/widgets/error_snakbar.dart';
|
||||
|
||||
class DurationController extends GetxController {
|
||||
final data = DurationData;
|
||||
// late AnimationController animationController;
|
||||
late List<MonthlyDataModel> rideData;
|
||||
late List<MonthlyRideModel> rideCountData;
|
||||
late List<MonthlyPriceDriverModel> ridePriceDriverData;
|
||||
Map<String, dynamic> jsonData1 = {};
|
||||
Map<String, dynamic> jsonData2 = {};
|
||||
bool isLoading = false;
|
||||
String totalDurationToday = '';
|
||||
var chartData;
|
||||
var chartRideCount;
|
||||
var chartRidePriceDriver;
|
||||
List monthlyList = [];
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
await fetchData();
|
||||
await fetchRideDriver();
|
||||
await getStaticDriver();
|
||||
}
|
||||
|
||||
getStaticDriver() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.driverStatistic,
|
||||
payload: {'driverID': box.read(BoxName.driverID)});
|
||||
|
||||
if (res == 'success') {
|
||||
try {
|
||||
monthlyList = jsonDecode(res)['message'];
|
||||
} catch (e) {
|
||||
monthlyList = [];
|
||||
}
|
||||
} else {
|
||||
monthlyList = [];
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> fetchData() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getTotalDriverDuration,
|
||||
payload: {'driver_id': box.read(BoxName.driverID)},
|
||||
);
|
||||
|
||||
if (res == 'success') {
|
||||
try {
|
||||
jsonData1 = jsonDecode(res);
|
||||
final List<dynamic> jsonData = jsonData1['message'];
|
||||
rideData = jsonData.map<MonthlyDataModel>((item) {
|
||||
return MonthlyDataModel.fromJson(item);
|
||||
}).toList();
|
||||
|
||||
final List<FlSpot> spots = rideData
|
||||
.map((data) => FlSpot(
|
||||
data.day.toDouble(),
|
||||
data.totalDuration.toDouble(),
|
||||
))
|
||||
.toList();
|
||||
chartData = spots;
|
||||
} catch (e) {
|
||||
jsonData1 = {};
|
||||
chartData = <FlSpot>[];
|
||||
}
|
||||
} else {
|
||||
jsonData1 = {};
|
||||
chartData = <FlSpot>[];
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> fetchRideDriver() async {
|
||||
isLoading = true;
|
||||
update(); // Notify the observers about the loading state change
|
||||
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getRidesDriverByDay,
|
||||
payload: {'driver_id': box.read(BoxName.driverID)},
|
||||
);
|
||||
if (res != 'failure' && res != 'no_internet' && res != 'token_expired') {
|
||||
jsonData2 = jsonDecode(res);
|
||||
var jsonResponse = jsonData2 as Map<String, dynamic>;
|
||||
isLoading = false;
|
||||
final List<dynamic> jsonData = jsonResponse['message'];
|
||||
rideCountData = jsonData.map<MonthlyRideModel>((item) {
|
||||
return MonthlyRideModel.fromJson(item);
|
||||
}).toList();
|
||||
ridePriceDriverData = jsonData.map<MonthlyPriceDriverModel>((item) {
|
||||
return MonthlyPriceDriverModel.fromJson(item);
|
||||
}).toList();
|
||||
|
||||
final List<FlSpot> spots = rideCountData
|
||||
.map((data) => FlSpot(
|
||||
data.day.toDouble(),
|
||||
data.countRide.toDouble(),
|
||||
))
|
||||
.toList();
|
||||
chartRideCount = spots;
|
||||
final List<FlSpot> spotsDriverPrices = ridePriceDriverData
|
||||
.map((data) => FlSpot(
|
||||
data.day.toDouble(),
|
||||
data.pricePerDay.toDouble(),
|
||||
))
|
||||
.toList();
|
||||
chartRidePriceDriver = spotsDriverPrices;
|
||||
|
||||
update();
|
||||
} else {
|
||||
isLoading = false;
|
||||
jsonData2 = {};
|
||||
chartRideCount = <FlSpot>[];
|
||||
chartRidePriceDriver = <FlSpot>[];
|
||||
update();
|
||||
|
||||
if (res == 'no_internet') {
|
||||
mySnackeBarError('No internet connection'.tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<DurationData> parseData(List<dynamic> json) {
|
||||
return json.map((entry) {
|
||||
final Map<String, dynamic> entryMap = entry;
|
||||
final day = DateTime.parse(entryMap['day']);
|
||||
final totalDuration = _parseDuration(entryMap['total_duration']);
|
||||
return DurationData(day, totalDuration);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Duration _parseDuration(String durationString) {
|
||||
final parts = durationString.split(':');
|
||||
final hours = int.parse(parts[0]);
|
||||
final minutes = int.parse(parts[1]);
|
||||
final seconds = int.parse(parts[2]);
|
||||
return Duration(hours: hours, minutes: minutes, seconds: seconds);
|
||||
}
|
||||
}
|
||||
|
||||
class DurationData {
|
||||
final DateTime day;
|
||||
final Duration totalDuration;
|
||||
|
||||
DurationData(this.day, this.totalDuration);
|
||||
}
|
||||
58
siro_driver/lib/controller/home/captin/help/assurance_controller.dart
Executable file
58
siro_driver/lib/controller/home/captin/help/assurance_controller.dart
Executable file
@@ -0,0 +1,58 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class AssuranceHealthController extends GetxController {
|
||||
bool isLoading = false;
|
||||
Map tripCount = {};
|
||||
|
||||
Future getTripCountByCaptain() async {
|
||||
var res = await CRUD().get(link: AppLink.getTripCountByCaptain, payload: {
|
||||
"driver_id": box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
if (res != 'failure') {
|
||||
tripCount = jsonDecode(res)['message'];
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addDriverHealthAssurance({
|
||||
String? driverId,
|
||||
String? assured,
|
||||
required String healthInsuranceProvider,
|
||||
}) async {
|
||||
// Define the URL to your PHP backend
|
||||
|
||||
// Data to be sent to the backend
|
||||
Map<String, String> data = {
|
||||
"driver_id": box.read(BoxName.driverID).toString(),
|
||||
"assured": '1',
|
||||
"health_insurance_provider": healthInsuranceProvider,
|
||||
};
|
||||
|
||||
try {
|
||||
// Send the POST request to your backend
|
||||
var response = await CRUD()
|
||||
.post(link: AppLink.addHealthInsuranceProvider, payload: data);
|
||||
|
||||
if (response != 'failure') {
|
||||
// Handle success (e.g., show a success message)
|
||||
|
||||
mySnackbarSuccess(
|
||||
"You have successfully opted for health insurance.".tr);
|
||||
} else {
|
||||
// Handle failure (e.g., show an error message)
|
||||
print("Failed to save health assurance data");
|
||||
mySnackeBarError("Please enter a health insurance status.".tr);
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle any errors
|
||||
print("Error: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
103
siro_driver/lib/controller/home/captin/help/help_controller.dart
Executable file
103
siro_driver/lib/controller/home/captin/help/help_controller.dart
Executable file
@@ -0,0 +1,103 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../../constant/box_name.dart';
|
||||
import '../../../../constant/links.dart';
|
||||
import '../../../../constant/style.dart';
|
||||
import '../../../../main.dart';
|
||||
import '../../../../views/widgets/elevated_btn.dart';
|
||||
import '../../../functions/crud.dart';
|
||||
import '../../../functions/encrypt_decrypt.dart';
|
||||
|
||||
class HelpController extends GetxController {
|
||||
bool isLoading = false;
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final helpQuestionController = TextEditingController();
|
||||
Map helpQuestionDate = {};
|
||||
Map helpQuestionRepleyDate = {};
|
||||
String status = '';
|
||||
String qustion = '';
|
||||
late int indexQuestion = 0;
|
||||
getIndex(int i, String qustion1) async {
|
||||
indexQuestion = i;
|
||||
qustion = qustion1;
|
||||
update();
|
||||
}
|
||||
|
||||
void addHelpQuestion() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
var res = await CRUD().post(link: AppLink.addhelpCenter, payload: {
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
'helpQuestion': (helpQuestionController.text)
|
||||
});
|
||||
var d = jsonDecode(res);
|
||||
isLoading = false;
|
||||
update();
|
||||
if (d['status'].toString() == 'success') {
|
||||
getHelpQuestion();
|
||||
// Get.snackbar('Feedback data saved successfully'.tr, '',
|
||||
// backgroundColor: AppColor.greenColor,
|
||||
// snackPosition: SnackPosition.BOTTOM);
|
||||
}
|
||||
}
|
||||
|
||||
void getHelpQuestion() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
var res = await CRUD().get(link: AppLink.gethelpCenter, payload: {
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
if (res == "failure") {
|
||||
isLoading = false;
|
||||
update();
|
||||
Get.defaultDialog(
|
||||
title: 'There is no help Question here'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
middleText: '',
|
||||
confirm: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
title: 'Add Question'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
}),
|
||||
MyElevatedButton(
|
||||
title: 'Back'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.back();
|
||||
}),
|
||||
],
|
||||
));
|
||||
}
|
||||
helpQuestionDate = jsonDecode(res);
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
Future getHelpRepley(String id) async {
|
||||
isLoading = true;
|
||||
update();
|
||||
var res = await CRUD().get(link: AppLink.getByIdhelpCenter, payload: {
|
||||
'id': id,
|
||||
});
|
||||
if (res == "failure") {
|
||||
status = 'not yet';
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
helpQuestionRepleyDate = jsonDecode(res);
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
getHelpQuestion();
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
22
siro_driver/lib/controller/home/captin/help/maintain_center_controller.dart
Executable file
22
siro_driver/lib/controller/home/captin/help/maintain_center_controller.dart
Executable file
@@ -0,0 +1,22 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MaintainCenterController extends GetxController {
|
||||
bool isLoading = false;
|
||||
Map tripCount = {};
|
||||
|
||||
Future getTripCountByCaptain() async {
|
||||
var res = await CRUD().get(link: AppLink.getTripCountByCaptain, payload: {
|
||||
"driver_id": box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
if (res != 'failure') {
|
||||
tripCount = jsonDecode(res)['message'];
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
69
siro_driver/lib/controller/home/captin/help/video_controller.dart
Executable file
69
siro_driver/lib/controller/home/captin/help/video_controller.dart
Executable file
@@ -0,0 +1,69 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/print.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class VideoController extends GetxController {
|
||||
var videos = [];
|
||||
var isLoading = true.obs;
|
||||
final String apiUrl =
|
||||
'${AppLink.seferCairoServer}/ride/videos_driver/get.php';
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
fetchVideos();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
late VideoPlayerController videoPlayerController;
|
||||
|
||||
// Initialize the video player with the provided URL
|
||||
Future<void> initializeVideo(String videoUrl) async {
|
||||
videoPlayerController =
|
||||
VideoPlayerController.networkUrl(Uri.parse(videoUrl));
|
||||
await videoPlayerController.initialize();
|
||||
videoPlayerController
|
||||
.setLooping(true); // Set to true if you want the video to loop
|
||||
update(); // Update the UI after the video has been initialized
|
||||
}
|
||||
|
||||
// Play the video
|
||||
void play() {
|
||||
videoPlayerController.play();
|
||||
update();
|
||||
}
|
||||
|
||||
// Pause the video
|
||||
void pause() {
|
||||
videoPlayerController.pause();
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
videoPlayerController
|
||||
.dispose(); // Dispose of the video player controller when not in use
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void fetchVideos() async {
|
||||
try {
|
||||
var res = await CRUD().get(link: apiUrl, payload: {});
|
||||
if (res != 'failure') {
|
||||
videos = jsonDecode(res)['message'];
|
||||
// Log.print('videos: ${videos}');
|
||||
update();
|
||||
} else {
|
||||
mySnackeBarError('');
|
||||
}
|
||||
} catch (e) {
|
||||
mySnackeBarError(e.toString());
|
||||
} finally {
|
||||
isLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
850
siro_driver/lib/controller/home/captin/home_captain_controller.dart
Executable file
850
siro_driver/lib/controller/home/captin/home_captain_controller.dart
Executable file
@@ -0,0 +1,850 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../constant/table_names.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../../views/home/my_wallet/walet_captain.dart';
|
||||
import '../../../views/widgets/elevated_btn.dart';
|
||||
import '../../firebase/firbase_messge.dart';
|
||||
import '../../functions/background_service.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/location_background_controller.dart';
|
||||
import '../../functions/location_controller.dart';
|
||||
import '../payment/captain_wallet_controller.dart';
|
||||
|
||||
class HomeCaptainController extends GetxController {
|
||||
bool isActive = false;
|
||||
DateTime? activeStartTime;
|
||||
Duration activeDuration = Duration.zero;
|
||||
Timer? activeTimer;
|
||||
Map data = {};
|
||||
bool isHomeMapActive = true;
|
||||
InlqBitmap carIcon = InlqBitmap.fromAsset('assets/images/car.png');
|
||||
bool isMapReadyForCommands = false;
|
||||
bool isLoading = true;
|
||||
late double kazan = 0;
|
||||
double latePrice = 0;
|
||||
double heavyPrice = 0;
|
||||
double comfortPrice = 0,
|
||||
speedPrice = 0,
|
||||
deliveryPrice = 0,
|
||||
mashwariPrice = 0,
|
||||
familyPrice = 0,
|
||||
fuelPrice = 0;
|
||||
double naturePrice = 0;
|
||||
bool isCallOn = false;
|
||||
String totalMoneyToday = '0';
|
||||
double? rating = 5;
|
||||
String rideId = '0';
|
||||
String countRideToday = '0';
|
||||
String totalMoneyInSEFER = '0';
|
||||
String totalDurationToday = '0';
|
||||
Timer? timer;
|
||||
Timer? _cameraFollowTimer;
|
||||
LatLng myLocation = const LatLng(33.5138, 36.2765);
|
||||
String totalPoints = '0';
|
||||
String countRefuse = '0';
|
||||
bool mapType = false;
|
||||
bool mapTrafficON = false;
|
||||
double widthMapTypeAndTraffic = 50;
|
||||
// === متغيرات الهيت ماب الجديدة ===
|
||||
bool isHeatmapVisible = false;
|
||||
Set<Polygon> heatmapPolygons =
|
||||
{}; // سنستخدم Polygon لرسم المربعات على جوجل مابس
|
||||
|
||||
// Inject the LocationController class
|
||||
// final locationController = Get.put(LocationController());
|
||||
// الكود الصحيح
|
||||
final locationController = Get.find<LocationController>();
|
||||
// final locationBackController = Get.put(LocationBackgroundController());
|
||||
String formatDuration(Duration duration) {
|
||||
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
|
||||
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
|
||||
return "${duration.inHours}:$twoDigitMinutes:$twoDigitSeconds";
|
||||
}
|
||||
|
||||
// دالة لتغيير حالة الهيت ماب (عرض/إخفاء)
|
||||
void toggleHeatmap() async {
|
||||
isHeatmapVisible = !isHeatmapVisible;
|
||||
print("🔥 [Heatmap] Visibility toggled to: $isHeatmapVisible");
|
||||
if (isHeatmapVisible) {
|
||||
startHeatmapCycle();
|
||||
} else {
|
||||
_heatmapTimer?.cancel();
|
||||
heatmapPolygons.clear();
|
||||
print("🧹 [Heatmap] Polygons cleared.");
|
||||
}
|
||||
update(); // تحديث الواجهة
|
||||
}
|
||||
|
||||
// داخل MapDriverController
|
||||
|
||||
// متغير لتخزين المربعات
|
||||
// Set<Polygon> heatmapPolygons = {};
|
||||
|
||||
// دالة جلب البيانات ورسم الخريطة
|
||||
Future<void> fetchAndDrawHeatmap() async {
|
||||
print("🚀 [Heatmap] Fetching live data...");
|
||||
// استخدم الرابط المباشر لملف JSON لسرعة قصوى
|
||||
final String jsonUrl =
|
||||
"https://ride.intaleq.xyz/intaleq/ride/heatmap_data.json";
|
||||
|
||||
try {
|
||||
// نستخدم timestamp لمنع الكاش من الموبايل نفسه
|
||||
final response = await http.get(
|
||||
Uri.parse("$jsonUrl?t=${DateTime.now().millisecondsSinceEpoch}"));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = json.decode(response.body);
|
||||
print("✅ [Heatmap] Data received. Points count: ${data.length}");
|
||||
_generatePolygons(data);
|
||||
} else {
|
||||
print("⚠️ [Heatmap] Server error: ${response.statusCode}");
|
||||
}
|
||||
} catch (e) {
|
||||
print("❌ [Heatmap] Error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void _generatePolygons(List<dynamic> data) {
|
||||
print("🎨 [Heatmap] Processing polygons...");
|
||||
Set<Polygon> tempPolygons = {};
|
||||
|
||||
// الأوفست لرسم المربع (نصف حجم الشبكة)
|
||||
// الشبكة دقتها 0.01 درجة، لذا نصفها 0.005
|
||||
double offset = 0.005;
|
||||
|
||||
int highCount = 0, medCount = 0, lowCount = 0;
|
||||
|
||||
for (var point in data) {
|
||||
double lat = double.parse(point['lat'].toString());
|
||||
double lng = double.parse(point['lng'].toString());
|
||||
|
||||
String intensity = point['intensity'] ?? 'low';
|
||||
int count = int.parse(point['count'].toString()); // ✅ جلب العدد
|
||||
|
||||
Color color;
|
||||
Color strokeColor;
|
||||
|
||||
// 🧠 منطق الألوان: ندمج الذكاء (Intensity) مع العدد (Count)
|
||||
if (intensity == 'high' || count >= 5) {
|
||||
highCount++;
|
||||
// منطقة مشتعلة (أحمر)
|
||||
// إما فيها طلبات ضائعة (Timeout) أو فيها عدد كبير من الطلبات
|
||||
color = Colors.red.withValues(alpha: 0.35);
|
||||
strokeColor = Colors.red.withValues(alpha: 0.8);
|
||||
} else if (intensity == 'medium' || count >= 3) {
|
||||
medCount++;
|
||||
// منطقة متوسطة (برتقالي)
|
||||
color = Colors.orange.withValues(alpha: 0.35);
|
||||
strokeColor = Colors.orange.withValues(alpha: 0.8);
|
||||
} else {
|
||||
lowCount++;
|
||||
// منطقة خفيفة (أصفر)
|
||||
color = Colors.yellow.withValues(alpha: 0.3);
|
||||
strokeColor = Colors.yellow.withValues(alpha: 0.6);
|
||||
}
|
||||
|
||||
// رسم المربع
|
||||
tempPolygons.add(Polygon(
|
||||
polygonId: PolygonId("$lat-$lng"),
|
||||
// consumeTapEvents: true, // للسماح بالضغط عليه مستقبلاً
|
||||
points: [
|
||||
LatLng(lat - offset, lng - offset),
|
||||
LatLng(lat + offset, lng - offset),
|
||||
LatLng(lat + offset, lng + offset),
|
||||
LatLng(lat - offset, lng + offset),
|
||||
],
|
||||
fillColor: color,
|
||||
strokeColor: strokeColor,
|
||||
strokeWidth: 2,
|
||||
));
|
||||
}
|
||||
|
||||
heatmapPolygons = tempPolygons;
|
||||
print(
|
||||
"✨ [Heatmap] Rendering Done. (🔥 High: $highCount, 🟠 Med: $medCount, 🟡 Low: $lowCount)");
|
||||
print("📍 [Heatmap] Total Polygons on Map: ${heatmapPolygons.length}");
|
||||
update(); // تحديث الخريطة
|
||||
}
|
||||
|
||||
Timer? _heatmapTimer;
|
||||
|
||||
// دالة لتشغيل الخريطة الحرارية كل فترة (كل 5 دقائق) لضمان نشاط البيانات
|
||||
void startHeatmapCycle() {
|
||||
_heatmapTimer?.cancel();
|
||||
fetchAndDrawHeatmap();
|
||||
|
||||
// Refresh every 15 min instead of 5 to reduce data & battery usage
|
||||
_heatmapTimer = Timer.periodic(const Duration(minutes: 15), (timer) {
|
||||
if (isHeatmapVisible) {
|
||||
print("🔄 [Heatmap] Periodic refresh started...");
|
||||
fetchAndDrawHeatmap();
|
||||
} else {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void goToWalletFromConnect() {
|
||||
Get.back();
|
||||
Get.back();
|
||||
Get.to(() => WalletCaptainRefactored());
|
||||
}
|
||||
|
||||
void changeRideId() {
|
||||
rideId = 'rideId';
|
||||
update();
|
||||
}
|
||||
|
||||
void addCustomCarIcon() {
|
||||
carIcon = InlqBitmap.fromAsset('assets/images/car.png');
|
||||
update();
|
||||
}
|
||||
|
||||
String stringActiveDuration = '';
|
||||
int _fatigueSeconds = 0; // عداد ثواني الإرهاق المؤقت
|
||||
|
||||
// ==========================================
|
||||
// ====== 🛡️ Fatigue Monitoring System ======
|
||||
// ==========================================
|
||||
void _checkFatigueBeforeOnline() {
|
||||
int totalSecondsToday = box.read('fatigue_total_seconds') ?? 0;
|
||||
String? lastOfflineStr = box.read('fatigue_last_offline');
|
||||
|
||||
if (lastOfflineStr != null) {
|
||||
DateTime lastOffline = DateTime.parse(lastOfflineStr);
|
||||
// If offline for more than 6 continuous hours, reset the fatigue counter
|
||||
if (DateTime.now().difference(lastOffline).inHours >= 6) {
|
||||
totalSecondsToday = 0;
|
||||
box.write('fatigue_total_seconds', 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (totalSecondsToday >= 12 * 3600) {
|
||||
// 12 Hours
|
||||
_forceOfflineDueToFatigue();
|
||||
throw Exception('Fatigue Limit Exceeded');
|
||||
}
|
||||
}
|
||||
|
||||
void _forceOfflineDueToFatigue() {
|
||||
if (isActive) {
|
||||
isActive = false;
|
||||
locationController.stopLocationUpdates();
|
||||
activeStartTime = null;
|
||||
activeTimer?.cancel();
|
||||
update();
|
||||
}
|
||||
|
||||
Get.defaultDialog(
|
||||
title: 'Safety First 🛑'.tr,
|
||||
middleText:
|
||||
'You have been driving for 12 hours. For your safety and compliance, please take a 6-hour break.'
|
||||
.tr,
|
||||
barrierDismissible: false,
|
||||
titleStyle:
|
||||
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||
confirm: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||
onPressed: () => Get.back(),
|
||||
child: Text('OK'.tr, style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void onButtonSelected() {
|
||||
if (!Get.isRegistered<CaptainWalletController>()) {
|
||||
Get.put(CaptainWalletController());
|
||||
}
|
||||
totalPoints = Get.find<CaptainWalletController>().totalPoints;
|
||||
|
||||
// Toggle Active State
|
||||
isActive = !isActive;
|
||||
|
||||
if (isActive) {
|
||||
try {
|
||||
_checkFatigueBeforeOnline(); // Throws exception if tired
|
||||
|
||||
if (double.parse(totalPoints) > -200) {
|
||||
locationController.startLocationUpdates();
|
||||
HapticFeedback.heavyImpact();
|
||||
activeStartTime = DateTime.now();
|
||||
|
||||
activeTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
activeDuration = DateTime.now().difference(activeStartTime!);
|
||||
stringActiveDuration = formatDuration(activeDuration);
|
||||
|
||||
// Increment Fatigue Counter (write to box every 30s)
|
||||
_fatigueSeconds++;
|
||||
if (_fatigueSeconds % 30 == 0) {
|
||||
int totalSeconds =
|
||||
(box.read('fatigue_total_seconds') ?? 0) + _fatigueSeconds;
|
||||
box.write('fatigue_total_seconds', totalSeconds);
|
||||
_fatigueSeconds = 0;
|
||||
if (totalSeconds >= 12 * 3600) {
|
||||
// 12 hours
|
||||
_forceOfflineDueToFatigue();
|
||||
}
|
||||
}
|
||||
|
||||
update();
|
||||
});
|
||||
} else {
|
||||
locationController.stopLocationUpdates();
|
||||
activeStartTime = null;
|
||||
activeTimer?.cancel();
|
||||
savePeriod(activeDuration);
|
||||
activeDuration = Duration.zero;
|
||||
box.write('fatigue_last_offline', DateTime.now().toIso8601String());
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
// Driver is fatigued, revert state
|
||||
isActive = false;
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
locationController.stopLocationUpdates();
|
||||
activeStartTime = null;
|
||||
activeTimer?.cancel();
|
||||
savePeriod(activeDuration);
|
||||
activeDuration = Duration.zero;
|
||||
|
||||
// Save offline time for Fatigue Monitoring reset
|
||||
box.write('fatigue_last_offline', DateTime.now().toIso8601String());
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
// متغيرات العداد للحظر
|
||||
RxString remainingBlockTimeStr = "".obs;
|
||||
Timer? _blockTimer;
|
||||
|
||||
/// دالة الفحص والدايلوج
|
||||
void checkAndShowBlockDialog() {
|
||||
String? blockStr = box.read(BoxName.blockUntilDate);
|
||||
if (blockStr == null || blockStr.isEmpty) return;
|
||||
|
||||
DateTime blockExpiry = DateTime.parse(blockStr);
|
||||
DateTime now = DateTime.now();
|
||||
|
||||
if (now.isBefore(blockExpiry)) {
|
||||
// 1. إجبار السائق على وضع الأوفلاين
|
||||
box.write(BoxName.statusDriverLocation, 'blocked');
|
||||
update();
|
||||
|
||||
// 2. بدء العداد
|
||||
_startBlockCountdown(blockExpiry);
|
||||
|
||||
// 3. إظهار الديالوج المانع
|
||||
Get.defaultDialog(
|
||||
title: "Your account is temporarily restricted ⛔".tr,
|
||||
titleStyle:
|
||||
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||
barrierDismissible: false, // 🚫 ممنوع الإغلاق بالضغط خارجاً
|
||||
onWillPop: () async => false, // 🚫 ممنوع زر الرجوع في الأندرويد
|
||||
content: Obx(() => Column(
|
||||
children: [
|
||||
const Icon(Icons.timer_off_outlined,
|
||||
size: 50, color: Colors.orange),
|
||||
const SizedBox(height: 15),
|
||||
Text(
|
||||
"You have exceeded the allowed cancellation limit (3 times).\nYou cannot work until the penalty expires."
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
remainingBlockTimeStr.value, // 🔥 الوقت يتحدث هنا
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
)),
|
||||
confirm: Obx(() {
|
||||
// الزر يكون مفعلاً فقط عندما ينتهي الوقت
|
||||
bool isFinished = remainingBlockTimeStr.value == "00:00:00" ||
|
||||
remainingBlockTimeStr.value == "Done";
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isFinished ? Colors.green : Colors.grey,
|
||||
),
|
||||
onPressed: isFinished
|
||||
? () {
|
||||
Get.back(); // إغلاق الديالوج
|
||||
box.remove(BoxName.blockUntilDate); // إزالة الحظر
|
||||
Get.snackbar("Welcome".tr, "You can now receive orders".tr,
|
||||
backgroundColor: Colors.green);
|
||||
}
|
||||
: null, // زر معطل
|
||||
child: Text(isFinished ? "Go Online".tr : "Wait for timer".tr),
|
||||
);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// الوقت انتهى أصلاً -> تنظيف
|
||||
box.remove(BoxName.blockUntilDate);
|
||||
}
|
||||
}
|
||||
|
||||
/// دالة العداد التنازلي
|
||||
void _startBlockCountdown(DateTime expiry) {
|
||||
_blockTimer?.cancel();
|
||||
_blockTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
DateTime now = DateTime.now();
|
||||
if (now.isAfter(expiry)) {
|
||||
// انتهى الوقت
|
||||
remainingBlockTimeStr.value = "Done";
|
||||
timer.cancel();
|
||||
} else {
|
||||
// حساب الفرق وتنسيقه
|
||||
Duration diff = expiry.difference(now);
|
||||
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||
String hours = twoDigits(diff.inHours);
|
||||
String minutes = twoDigits(diff.inMinutes.remainder(60));
|
||||
String seconds = twoDigits(diff.inSeconds.remainder(60));
|
||||
|
||||
remainingBlockTimeStr.value = "$hours:$minutes:$seconds";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
print("🔥 [HomeCaptain] onClose called. Tearing down map resources...");
|
||||
_blockTimer?.cancel();
|
||||
activeTimer?.cancel();
|
||||
_cameraFollowTimer?.cancel();
|
||||
_heatmapTimer?.cancel();
|
||||
stopTimer();
|
||||
mapHomeCaptainController = null;
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
void getRefusedOrderByCaptain() async {
|
||||
DateTime today = DateTime.now();
|
||||
int todayDay = today.day;
|
||||
|
||||
String driverId = box.read(BoxName.driverID).toString();
|
||||
|
||||
String customQuery = '''
|
||||
SELECT COUNT(*) AS count
|
||||
FROM ${TableName.driverOrdersRefuse}
|
||||
WHERE driver_id = '$driverId'
|
||||
AND created_at LIKE '%$todayDay%'
|
||||
''';
|
||||
|
||||
try {
|
||||
List<Map<String, dynamic>> results =
|
||||
await sql.getCustomQuery(customQuery);
|
||||
countRefuse = results[0]['count'].toString();
|
||||
update();
|
||||
if (double.parse(totalPoints) <= -200) {
|
||||
// if (int.parse(countRefuse) > 3 || double.parse(totalPoints) <= -200) {
|
||||
locationController.stopLocationUpdates();
|
||||
activeStartTime = null;
|
||||
activeTimer?.cancel();
|
||||
savePeriod(activeDuration);
|
||||
activeDuration = Duration.zero;
|
||||
update();
|
||||
|
||||
Get.defaultDialog(
|
||||
// backgroundColor: CupertinoColors.destructiveRed,
|
||||
barrierDismissible: false,
|
||||
title: 'You Are Stopped For this Day !'.tr,
|
||||
content: Text(
|
||||
'You Refused 3 Rides this Day that is the reason \nSee you Tomorrow!'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok , See you Tomorrow'.tr,
|
||||
onPressed: () {
|
||||
// إغلاق الديالوج والعودة قسرياً
|
||||
navigatorKey.currentState?.pop();
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
void changeMapType() {
|
||||
mapType = !mapType;
|
||||
// heightButtomSheetShown = isButtomSheetShown == true ? 240 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeMapTraffic() {
|
||||
mapTrafficON = !mapTrafficON;
|
||||
update();
|
||||
}
|
||||
|
||||
// late IntaleqMapController mapHomeCaptainController;
|
||||
IntaleqMapController? mapHomeCaptainController;
|
||||
LatLng? _lastCameraLoc; // لتتبع آخر موقع حرك الكاميرا
|
||||
|
||||
// --- FIX 2: Smart Map Creation ---
|
||||
void onMapCreated(IntaleqMapController controller) {
|
||||
print("🔥 [HomeCaptain] onMapCreated started");
|
||||
mapHomeCaptainController = controller;
|
||||
|
||||
// We delay the first move to ensure the native side is fully ready
|
||||
Future.delayed(const Duration(milliseconds: 800), () {
|
||||
if (isClosed || mapHomeCaptainController == null) return;
|
||||
|
||||
try {
|
||||
var currentLoc = locationController.myLocation;
|
||||
if (currentLoc.latitude != 0 &&
|
||||
currentLoc.latitude != null &&
|
||||
!currentLoc.latitude.isNaN) {
|
||||
print(
|
||||
"🔥 [HomeCaptain] Safely moving camera to: ${currentLoc.latitude}");
|
||||
mapHomeCaptainController!.moveCamera(
|
||||
CameraUpdate.newLatLngZoom(currentLoc, 17.5),
|
||||
);
|
||||
} else {
|
||||
print("🔥 [HomeCaptain] Safely moving to default Damascus");
|
||||
mapHomeCaptainController!.moveCamera(
|
||||
CameraUpdate.newLatLngZoom(myLocation, 12),
|
||||
);
|
||||
}
|
||||
// Mark as ready for regular listener updates
|
||||
isMapReadyForCommands = true;
|
||||
} catch (e) {
|
||||
print("❌ [HomeCaptain] Map move failed: $e");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void savePeriod(Duration period) {
|
||||
final periods = box.read<List<dynamic>>(BoxName.periods) ?? [];
|
||||
periods.add(period.inSeconds);
|
||||
box.write(BoxName.periods, periods);
|
||||
}
|
||||
|
||||
Duration calculateTotalDuration() {
|
||||
final periods = box.read<List<dynamic>>(BoxName.periods) ?? [];
|
||||
Duration totalDuration = Duration.zero;
|
||||
for (dynamic periodInSeconds in periods) {
|
||||
final periodDuration = Duration(seconds: periodInSeconds);
|
||||
totalDuration += periodDuration;
|
||||
}
|
||||
return totalDuration;
|
||||
}
|
||||
|
||||
Timer? _localDurationTimer;
|
||||
RxString totalDurationDisplay = "00:00:00".obs; // لعرض الوقت في الواجهة
|
||||
Duration _currentDuration = Duration.zero; // لتخزين الوقت ككائن Duration
|
||||
|
||||
void startPeriodicExecution() async {
|
||||
await getCaptainDurationOnToday();
|
||||
String? initialDurationStr = totalDurationToday;
|
||||
|
||||
if (initialDurationStr != '0') {
|
||||
// تحويل النص (01:20:30) إلى كائن Duration
|
||||
List<String> parts = initialDurationStr.split(':');
|
||||
_currentDuration = Duration(
|
||||
hours: int.parse(parts[0]),
|
||||
minutes: int.parse(parts[1]),
|
||||
seconds: int.parse(parts[2]),
|
||||
);
|
||||
|
||||
// بدء العداد المحلي
|
||||
_startLocalClock();
|
||||
}
|
||||
|
||||
// Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||
// await getCaptainDurationOnToday();
|
||||
// });
|
||||
}
|
||||
|
||||
void _startLocalClock() {
|
||||
_localDurationTimer?.cancel();
|
||||
_localDurationTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
// زيادة ثانية واحدة محلياً
|
||||
_currentDuration += const Duration(seconds: 1);
|
||||
|
||||
// تحديث النص المعروض في الواجهة (Formatting)
|
||||
totalDurationDisplay.value = _formatDuration(_currentDuration);
|
||||
|
||||
// اختيارياً: كل 5 دقائق فقط، قم بتحديث القيمة من السيرفر للتأكد من المزامنة
|
||||
if (timer.tick % 300 == 0) {
|
||||
getCaptainDurationOnToday();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String _formatDuration(Duration duration) {
|
||||
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||
String hours = twoDigits(duration.inHours);
|
||||
String minutes = twoDigits(duration.inMinutes.remainder(60));
|
||||
String seconds = twoDigits(duration.inSeconds.remainder(60));
|
||||
return "$hours:$minutes:$seconds";
|
||||
}
|
||||
|
||||
void stopTimer() {
|
||||
_localDurationTimer?.cancel();
|
||||
}
|
||||
|
||||
getlocation() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
try {
|
||||
// ننتظر جلب الموقع مع مهلة 10 ثوانٍ لتجنب التعليق
|
||||
var locData = await locationController.getLocation().timeout(
|
||||
const Duration(seconds: 10),
|
||||
onTimeout: () => null,
|
||||
);
|
||||
|
||||
if (locData != null && locData.latitude != null) {
|
||||
myLocation = LatLng(locData.latitude!, locData.longitude!);
|
||||
print(
|
||||
"📍 [HomeCaptain] Location updated: ${myLocation.latitude}, ${myLocation.longitude}");
|
||||
} else {
|
||||
print(
|
||||
"⚠️ [HomeCaptain] Could not get current location, using default.");
|
||||
}
|
||||
} catch (e) {
|
||||
print("❌ Error in getlocation: $e");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
Map walletDriverPointsDate = {};
|
||||
|
||||
Future getCaptainWalletFromBuyPoints() async {
|
||||
// isLoading = true;
|
||||
update();
|
||||
|
||||
var res = await CRUD().getWallet(
|
||||
link: AppLink.getDriverPaymentPoints,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
isLoading = false;
|
||||
// update();
|
||||
|
||||
if (res != 'failure') {
|
||||
walletDriverPointsDate = jsonDecode(res);
|
||||
double totalPointsDouble = double.parse(
|
||||
walletDriverPointsDate['message'][0]['total_amount'].toString());
|
||||
totalPoints = totalPointsDouble.toStringAsFixed(0);
|
||||
update();
|
||||
} else {
|
||||
totalPoints = '0';
|
||||
}
|
||||
}
|
||||
|
||||
// 3. دالة نستدعيها عند قبول الطلب
|
||||
void pauseHomeMapUpdates() {
|
||||
isHomeMapActive = false;
|
||||
update();
|
||||
}
|
||||
|
||||
// 4. دالة نستدعيها عند العودة للصفحة الرئيسية
|
||||
void resumeHomeMapUpdates() {
|
||||
isHomeMapActive = true;
|
||||
// تم حذف استدعاء onMapCreated المتكرر لمنع قفز الخريطة عند العودة
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
// ✅ تم إرجاعه كتعليق لمنع الديالوج عند التشغيل (كما كان في الكود الأصلي)
|
||||
// bool permissionsGranted = await PermissionsHelper.requestAllPermissions();
|
||||
// if (permissionsGranted) {
|
||||
// await BackgroundServiceHelper.startService();
|
||||
// }
|
||||
|
||||
Get.put(FirebaseMessagesController());
|
||||
addToken();
|
||||
await getlocation();
|
||||
onButtonSelected();
|
||||
getDriverRate();
|
||||
addCustomCarIcon();
|
||||
getKazanPercent();
|
||||
getPaymentToday();
|
||||
getCountRideToday();
|
||||
getAllPayment();
|
||||
startPeriodicExecution();
|
||||
getCaptainWalletFromBuyPoints();
|
||||
// onMapCreated(mapHomeCaptainController!);
|
||||
// totalPoints = Get.find<CaptainWalletController>().totalPoints.toString();
|
||||
// getRefusedOrderByCaptain();
|
||||
// 🔥 الفحص عند تشغيل التطبيق
|
||||
checkAndShowBlockDialog();
|
||||
box.write(BoxName.statusDriverLocation, 'off');
|
||||
// 2. عدل الليسنر ليصبح مشروطاً
|
||||
// Camera follow timer — only moves when the driver has
|
||||
// actually moved > 15 meters, saving GPU/battery on idle.
|
||||
_cameraFollowTimer = Timer.periodic(const Duration(seconds: 8), (timer) {
|
||||
if (isClosed ||
|
||||
!isHomeMapActive ||
|
||||
mapHomeCaptainController == null ||
|
||||
!isMapReadyForCommands ||
|
||||
!isActive) return;
|
||||
|
||||
var loc = locationController.myLocation;
|
||||
if (loc.latitude != 0 && loc.latitude != null && !loc.latitude.isNaN) {
|
||||
// Skip if driver hasn't moved significantly
|
||||
if (_lastCameraLoc != null) {
|
||||
final double dist = Geolocator.distanceBetween(
|
||||
_lastCameraLoc!.latitude,
|
||||
_lastCameraLoc!.longitude,
|
||||
loc.latitude,
|
||||
loc.longitude,
|
||||
);
|
||||
if (dist < 15) return;
|
||||
}
|
||||
_lastCameraLoc = loc;
|
||||
try {
|
||||
if (mapHomeCaptainController != null) {
|
||||
mapHomeCaptainController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(loc, 17.5),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print("❌ [HomeCaptain] Camera movement failed: $e");
|
||||
}
|
||||
}
|
||||
});
|
||||
// LocationController().getLocation();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
addToken() async {
|
||||
String? fingerPrint = await storage.read(key: BoxName.fingerPrint);
|
||||
final payload = {
|
||||
'token': (box.read(BoxName.tokenDriver)),
|
||||
'captain_id': (box.read(BoxName.driverID)).toString(),
|
||||
'fingerPrint': (fingerPrint).toString()
|
||||
};
|
||||
// Log.print('payload: ${payload}');
|
||||
CRUD().post(link: AppLink.addTokensDriver, payload: payload);
|
||||
}
|
||||
|
||||
getPaymentToday() async {
|
||||
var res = await CRUD().getWallet(
|
||||
link: AppLink.getDriverPaymentToday,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()});
|
||||
if (res != 'failure') {
|
||||
data = jsonDecode(res);
|
||||
totalMoneyToday = data['message'][0]['todayAmount'].toString();
|
||||
|
||||
update();
|
||||
} else {}
|
||||
}
|
||||
|
||||
getKazanPercent() async {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getKazanPercent,
|
||||
payload: {'country': box.read(BoxName.countryCode).toString()},
|
||||
);
|
||||
if (res != 'failure') {
|
||||
var json = jsonDecode(res);
|
||||
kazan = double.parse(json['message'][0]['kazan']);
|
||||
naturePrice = double.parse(json['message'][0]['naturePrice']);
|
||||
heavyPrice = double.parse(json['message'][0]['heavyPrice']);
|
||||
latePrice = double.parse(json['message'][0]['latePrice']);
|
||||
comfortPrice = double.parse(json['message'][0]['comfortPrice']);
|
||||
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();
|
||||
}
|
||||
|
||||
double mpg = 0;
|
||||
calculateConsumptionFuel() {
|
||||
mpg = fuelPrice / 12; //todo in register car add mpg in box
|
||||
}
|
||||
|
||||
getCountRideToday() async {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getCountRide,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()});
|
||||
data = jsonDecode(res);
|
||||
|
||||
countRideToday = data['message'][0]['count'].toString();
|
||||
update();
|
||||
}
|
||||
|
||||
getDriverRate() async {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getDriverRate,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()});
|
||||
if (res != 'failure') {
|
||||
var decod = jsonDecode(res);
|
||||
if (decod['message'][0]['rating'] != null) {
|
||||
rating = double.parse(decod['message'][0]['rating'].toString());
|
||||
} else {
|
||||
rating = 5.0; // Set a default value (e.g., 5.0 for full rating)
|
||||
}
|
||||
} else {
|
||||
rating = 5;
|
||||
}
|
||||
}
|
||||
|
||||
getAllPayment() async {
|
||||
var res = await CRUD().getWallet(
|
||||
link: AppLink.getAllPaymentFromRide,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()});
|
||||
if (res == 'failure') {
|
||||
totalMoneyInSEFER = '0';
|
||||
} else {
|
||||
data = jsonDecode(res);
|
||||
|
||||
totalMoneyInSEFER = data['message'][0]['total_amount'];
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void changeToAppliedRide(String status) {
|
||||
box.write(BoxName.rideStatus, status);
|
||||
Log.print('rideStatus from homcaptain : ${box.read(BoxName.rideStatus)}');
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> getCaptainDurationOnToday() async {
|
||||
try {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getTotalDriverDurationToday,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
|
||||
if (res == null || res == 'failure') {
|
||||
totalDurationToday = '0';
|
||||
update();
|
||||
return;
|
||||
}
|
||||
|
||||
var data = jsonDecode(res);
|
||||
totalDurationToday = data['message']?[0]?['total_duration'] ?? '0';
|
||||
} catch (e) {
|
||||
print('Error in getCaptainDurationOnToday: $e');
|
||||
totalDurationToday = '0';
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
}
|
||||
2797
siro_driver/lib/controller/home/captin/map_driver_controller.dart
Executable file
2797
siro_driver/lib/controller/home/captin/map_driver_controller.dart
Executable file
File diff suppressed because it is too large
Load Diff
86
siro_driver/lib/controller/home/captin/model.dart
Normal file
86
siro_driver/lib/controller/home/captin/model.dart
Normal file
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
class NavigationStep {
|
||||
final String instruction;
|
||||
final String maneuver;
|
||||
final double distance;
|
||||
final String duration;
|
||||
final LatLng startLocation;
|
||||
final LatLng endLocation;
|
||||
final String htmlInstructions;
|
||||
|
||||
NavigationStep({
|
||||
required this.instruction,
|
||||
required this.maneuver,
|
||||
required this.distance,
|
||||
required this.duration,
|
||||
required this.startLocation,
|
||||
required this.endLocation,
|
||||
required this.htmlInstructions,
|
||||
});
|
||||
|
||||
factory NavigationStep.fromJson(Map<String, dynamic> json) {
|
||||
return NavigationStep(
|
||||
instruction: json['html_instructions'] ?? '',
|
||||
maneuver: json['maneuver'] ?? 'straight',
|
||||
distance: (json['distance']['value'] ?? 0).toDouble(),
|
||||
duration: json['duration']['text'] ?? '',
|
||||
startLocation: LatLng(
|
||||
json['start_location']['lat'].toDouble(),
|
||||
json['start_location']['lng'].toDouble(),
|
||||
),
|
||||
endLocation: LatLng(
|
||||
json['end_location']['lat'].toDouble(),
|
||||
json['end_location']['lng'].toDouble(),
|
||||
),
|
||||
htmlInstructions: json['html_instructions'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
// Get clean instruction text (remove HTML tags)
|
||||
String get cleanInstruction {
|
||||
return instruction
|
||||
.replaceAll(RegExp(r'<[^>]*>'), '')
|
||||
.replaceAll(' ', ' ');
|
||||
}
|
||||
|
||||
// Get instruction icon based on maneuver
|
||||
IconData get instructionIcon {
|
||||
switch (maneuver.toLowerCase()) {
|
||||
case 'turn-left':
|
||||
return Icons.turn_left;
|
||||
case 'turn-right':
|
||||
return Icons.turn_right;
|
||||
case 'turn-slight-left':
|
||||
return Icons.turn_slight_left;
|
||||
case 'turn-slight-right':
|
||||
return Icons.turn_slight_right;
|
||||
case 'turn-sharp-left':
|
||||
return Icons.turn_sharp_left;
|
||||
case 'turn-sharp-right':
|
||||
return Icons.turn_sharp_right;
|
||||
case 'uturn-left':
|
||||
case 'uturn-right':
|
||||
return Icons.u_turn_left;
|
||||
case 'straight':
|
||||
return Icons.straight;
|
||||
case 'ramp-left':
|
||||
return Icons.ramp_left;
|
||||
case 'ramp-right':
|
||||
return Icons.ramp_right;
|
||||
case 'merge':
|
||||
return Icons.merge;
|
||||
case 'fork-left':
|
||||
case 'fork-right':
|
||||
return Icons.call_split;
|
||||
case 'ferry':
|
||||
return Icons.directions_boat;
|
||||
case 'roundabout-left':
|
||||
case 'roundabout-right':
|
||||
return Icons.roundabout_left;
|
||||
default:
|
||||
return Icons.navigation;
|
||||
}
|
||||
}
|
||||
}
|
||||
101
siro_driver/lib/controller/home/captin/navigation_service.dart
Normal file
101
siro_driver/lib/controller/home/captin/navigation_service.dart
Normal file
@@ -0,0 +1,101 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_driver/constant/api_key.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/controller/functions/tts.dart';
|
||||
|
||||
import '../../../main.dart';
|
||||
|
||||
/// Handles map-related logic: fetching routes, drawing polylines, and managing markers.
|
||||
class NavigationService extends GetxService {
|
||||
final CRUD _crud = CRUD();
|
||||
final TextToSpeechController _tts = Get.put(TextToSpeechController());
|
||||
|
||||
final RxSet<Marker> markers = <Marker>{}.obs;
|
||||
final RxSet<Polyline> polylines = <Polyline>{}.obs;
|
||||
final RxString currentInstruction = "".obs;
|
||||
|
||||
InlqBitmap carIcon = InlqBitmap.defaultMarker;
|
||||
InlqBitmap passengerIcon = InlqBitmap.defaultMarker;
|
||||
InlqBitmap startIcon = InlqBitmap.defaultMarker;
|
||||
InlqBitmap endIcon = InlqBitmap.defaultMarker;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_loadCustomIcons();
|
||||
}
|
||||
|
||||
void _loadCustomIcons() {
|
||||
carIcon = InlqBitmap.fromAsset('assets/images/car.png');
|
||||
passengerIcon = InlqBitmap.fromAsset('assets/images/picker.png');
|
||||
startIcon = InlqBitmap.fromAsset('assets/images/A.png');
|
||||
endIcon = InlqBitmap.fromAsset('assets/images/b.png');
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getRoute({
|
||||
required LatLng origin,
|
||||
required LatLng destination,
|
||||
}) async {
|
||||
final url =
|
||||
'${AppLink.googleMapsLink}directions/json?language=${box.read(BoxName.lang)}&destination=${destination.latitude},${destination.longitude}&origin=${origin.latitude},${origin.longitude}&key=${AK.mapAPIKEY}';
|
||||
|
||||
final response = await _crud.getGoogleApi(link: url, payload: {});
|
||||
|
||||
if (response != null && response['routes'].isNotEmpty) {
|
||||
return response['routes'][0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void drawRoute(Map<String, dynamic> routeData, {Color color = Colors.blue}) {
|
||||
final pointsString = routeData["overview_polyline"]["points"];
|
||||
final points = PolylineUtils.decode(pointsString);
|
||||
|
||||
final polyline = Polyline(
|
||||
polylineId: PolylineId(routeData["summary"] ?? DateTime.now().toString()),
|
||||
points: points,
|
||||
width: 8,
|
||||
color: color,
|
||||
);
|
||||
|
||||
polylines.add(polyline);
|
||||
}
|
||||
|
||||
void updateCarMarker(LatLng position, double heading) {
|
||||
markers.removeWhere((m) => m.markerId.value == 'MyLocation');
|
||||
markers.add(
|
||||
Marker(
|
||||
markerId: MarkerId('MyLocation'.tr),
|
||||
position: position,
|
||||
icon: carIcon,
|
||||
rotation: heading,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
flat: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void setInitialMarkers(
|
||||
LatLng passengerLocation, LatLng passengerDestination) {
|
||||
markers.clear();
|
||||
markers.add(Marker(
|
||||
markerId: const MarkerId('passengerLocation'),
|
||||
position: passengerLocation,
|
||||
icon: passengerIcon,
|
||||
));
|
||||
markers.add(Marker(
|
||||
markerId: const MarkerId('passengerDestination'),
|
||||
position: passengerDestination,
|
||||
icon: endIcon,
|
||||
));
|
||||
}
|
||||
|
||||
void clearRoutes() {
|
||||
polylines.clear();
|
||||
currentInstruction.value = "";
|
||||
}
|
||||
}
|
||||
742
siro_driver/lib/controller/home/captin/order_request_controller.dart
Executable file
742
siro_driver/lib/controller/home/captin/order_request_controller.dart
Executable file
@@ -0,0 +1,742 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../env/env.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../../views/home/Captin/driver_map_page.dart';
|
||||
import '../../../views/home/Captin/orderCaptin/marker_generator.dart';
|
||||
import '../../../views/widgets/mydialoug.dart';
|
||||
import '../../firebase/local_notification.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/location_controller.dart';
|
||||
import '../../home/captin/home_captain_controller.dart';
|
||||
|
||||
class OrderRequestController extends GetxController
|
||||
with WidgetsBindingObserver {
|
||||
// --- متغيرات التايمر ---
|
||||
double progress = 1.0;
|
||||
int duration = 15;
|
||||
int remainingTime = 15;
|
||||
Timer? _timer;
|
||||
|
||||
bool applied = false;
|
||||
final locationController = Get.put(LocationController());
|
||||
|
||||
// 🔥 متغير لمنع تكرار القبول
|
||||
bool _isRideTakenHandled = false;
|
||||
|
||||
// --- الأيقونات والماركرز ---
|
||||
InlqBitmap? driverIcon;
|
||||
Map<MarkerId, Marker> markersMap = {};
|
||||
Set<Marker> get markers => markersMap.values.toSet();
|
||||
|
||||
// --- البيانات والتحكم ---
|
||||
// 🔥 تم إضافة myMapData لدعم السوكيت الجديد
|
||||
List<dynamic>? myList;
|
||||
Map<dynamic, dynamic>? myMapData;
|
||||
|
||||
IntaleqMapController? mapController;
|
||||
|
||||
// الإحداثيات (أزلنا late لتجنب الأخطاء القاتلة)
|
||||
double latPassenger = 0.0;
|
||||
double lngPassenger = 0.0;
|
||||
double latDestination = 0.0;
|
||||
double lngDestination = 0.0;
|
||||
|
||||
// --- متغيرات العرض ---
|
||||
String passengerRating = "5.0";
|
||||
String tripType = "Standard";
|
||||
String totalTripDistance = "--";
|
||||
String totalTripDuration = "--";
|
||||
String tripPrice = "--";
|
||||
|
||||
String timeToPassenger = "Calculating...".tr;
|
||||
String distanceToPassenger = "--";
|
||||
|
||||
// --- الخريطة ---
|
||||
Set<Polyline> polylines = {};
|
||||
|
||||
// حالة التطبيق والصوت
|
||||
bool isInBackground = false;
|
||||
final AudioPlayer audioPlayer = AudioPlayer();
|
||||
|
||||
@override
|
||||
Future<void> onInit() async {
|
||||
// 🛑 حماية من الفتح المتكرر لنفس الطلب
|
||||
if (Get.arguments == null) {
|
||||
print("❌ OrderController Error: No arguments received.");
|
||||
Get.back(); // إغلاق الصفحة فوراً
|
||||
return;
|
||||
}
|
||||
super.onInit();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
_checkOverlay();
|
||||
|
||||
// 🔥 تهيئة البيانات هي الخطوة الأولى والأهم
|
||||
_initializeData();
|
||||
_parseExtraData();
|
||||
|
||||
// 1. تجهيز أيقونة السائق
|
||||
await _prepareDriverIcon();
|
||||
|
||||
// 2. وضع الماركرز المبدئية
|
||||
_updateMarkers(
|
||||
paxTime: "...",
|
||||
paxDist: "",
|
||||
destTime: totalTripDuration,
|
||||
destDist: totalTripDistance);
|
||||
|
||||
// 3. رسم مبدئي
|
||||
_initialMapSetup();
|
||||
|
||||
// 4. الاستماع للسوكيت
|
||||
_listenForRideTaken();
|
||||
|
||||
// 5. حساب المسارين
|
||||
await _calculateFullJourney();
|
||||
|
||||
// 6. تشغيل التايمر
|
||||
startTimer();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 🔥🔥🔥 Smart Data Handling (List & Map Support) 🔥🔥🔥
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
void _initializeData() {
|
||||
var args = Get.arguments;
|
||||
print("📦 Order Controller Received Type: ${args.runtimeType}");
|
||||
print("📦 Order Controller Data: $args");
|
||||
|
||||
if (args != null) {
|
||||
// الحالة 1: قائمة مباشرة (Legacy / Some Firebase formats)
|
||||
if (args is List) {
|
||||
myList = args;
|
||||
}
|
||||
// الحالة 2: خريطة (Map)
|
||||
else if (args is Map) {
|
||||
// أ) هل هي قادمة من Firebase وتحتوي على DriverList؟
|
||||
if (args.containsKey('DriverList')) {
|
||||
var listData = args['DriverList'];
|
||||
if (listData is List) {
|
||||
myList = listData;
|
||||
} else if (listData is String) {
|
||||
// أحياناً تصل كنص مشفر داخل الـ Map
|
||||
try {
|
||||
myList = jsonDecode(listData);
|
||||
} catch (e) {
|
||||
print("Error decoding DriverList: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
// ب) هل هي قادمة من Socket بالمفاتيح الرقمية ("0", "1", ...)؟
|
||||
else {
|
||||
myMapData = args;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// تعبئة الإحداثيات باستخدام الدالة الذكية _getValueAt
|
||||
latPassenger = _parseCoord(_getValueAt(0));
|
||||
lngPassenger = _parseCoord(_getValueAt(1));
|
||||
latDestination = _parseCoord(_getValueAt(3));
|
||||
lngDestination = _parseCoord(_getValueAt(4));
|
||||
|
||||
print(
|
||||
"📍 Parsed Coordinates: Pax($latPassenger, $lngPassenger) -> Dest($latDestination, $lngDestination)");
|
||||
}
|
||||
|
||||
/// 🔥 دالة ذكية تجلب القيمة سواء كانت البيانات في List أو Map
|
||||
dynamic _getValueAt(int index) {
|
||||
// الأولوية للقائمة
|
||||
if (myList != null && index < myList!.length) {
|
||||
return myList![index];
|
||||
}
|
||||
// ثم الخريطة (السوكيت) - المفاتيح عبارة عن String
|
||||
if (myMapData != null && myMapData!.containsKey(index.toString())) {
|
||||
return myMapData![index.toString()];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// الدالة التي يستخدمها باقي الكود لجلب البيانات كنصوص
|
||||
String _safeGet(int index) {
|
||||
var val = _getValueAt(index);
|
||||
if (val != null) {
|
||||
return val.toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
double _parseCoord(dynamic val) {
|
||||
if (val == null) return 0.0;
|
||||
String s = val.toString().replaceAll(',', '').trim();
|
||||
if (s.contains(' ')) s = s.split(' ')[0];
|
||||
return double.tryParse(s) ?? 0.0;
|
||||
}
|
||||
|
||||
void _parseExtraData() {
|
||||
passengerRating = _safeGet(33).isEmpty ? "5.0" : _safeGet(33);
|
||||
tripType = _safeGet(31);
|
||||
|
||||
// Format numbers to avoid many decimal places
|
||||
String rawDist = _safeGet(5);
|
||||
if (rawDist.isNotEmpty) {
|
||||
double? d = double.tryParse(rawDist);
|
||||
totalTripDistance = d != null ? "${d.toStringAsFixed(1)} km" : rawDist;
|
||||
}
|
||||
|
||||
String rawDur = _safeGet(19);
|
||||
if (rawDur.isNotEmpty) {
|
||||
double? d = double.tryParse(rawDur);
|
||||
totalTripDuration = d != null ? "${d.toStringAsFixed(0)} min" : rawDur;
|
||||
}
|
||||
|
||||
String rawPrice = _safeGet(2);
|
||||
if (rawPrice.isNotEmpty) {
|
||||
double? p = double.tryParse(rawPrice);
|
||||
tripPrice = p != null ? p.toStringAsFixed(0) : rawPrice;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 🔥🔥🔥 Core Logic: Concurrent API Calls & Bounds 🔥🔥🔥
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
Future<void> _calculateFullJourney() async {
|
||||
// Don't block on mapController being null - we'll draw routes
|
||||
// and markers first, then zoom when controller is ready
|
||||
bool canZoom = mapController != null;
|
||||
|
||||
try {
|
||||
// Reuse stored location from LocationController instead of
|
||||
// making a duplicate GPS hardware call (already fetched in
|
||||
// _initialMapSetup).
|
||||
LatLng driverLatLng;
|
||||
double driverHeading = 0.0;
|
||||
if (Get.isRegistered<LocationController>()) {
|
||||
final locCtrl = Get.find<LocationController>();
|
||||
if (locCtrl.myLocation.latitude != 0 ||
|
||||
locCtrl.myLocation.longitude != 0) {
|
||||
driverLatLng = locCtrl.myLocation;
|
||||
driverHeading = locCtrl.heading;
|
||||
} else {
|
||||
Position driverPos = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
|
||||
driverHeading = driverPos.heading;
|
||||
}
|
||||
} else {
|
||||
Position driverPos = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high);
|
||||
driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
|
||||
driverHeading = driverPos.heading;
|
||||
}
|
||||
|
||||
updateDriverLocation(driverLatLng, driverHeading);
|
||||
|
||||
// Clear old polylines to avoid "ghost lines"
|
||||
polylines.clear();
|
||||
|
||||
var pickupFuture = _fetchRouteData(
|
||||
start: driverLatLng,
|
||||
end: LatLng(latPassenger, lngPassenger),
|
||||
color: Colors.amber,
|
||||
id: 'pickup_route');
|
||||
|
||||
var tripFuture = _fetchRouteData(
|
||||
start: LatLng(latPassenger, lngPassenger),
|
||||
end: LatLng(latDestination, lngDestination),
|
||||
color: Colors.black,
|
||||
id: 'trip_route',
|
||||
getSteps: true); // 🔥 نطلب الخطوات للمسار
|
||||
|
||||
var results = await Future.wait([pickupFuture, tripFuture]);
|
||||
|
||||
var pickupResult = results[0];
|
||||
var tripResult = results[1];
|
||||
|
||||
if (pickupResult != null) {
|
||||
distanceToPassenger = pickupResult['distance_text'];
|
||||
timeToPassenger = pickupResult['duration_text'];
|
||||
polylines.add(pickupResult['polyline']);
|
||||
}
|
||||
|
||||
if (tripResult != null) {
|
||||
totalTripDistance = tripResult['distance_text'];
|
||||
totalTripDuration = tripResult['duration_text'];
|
||||
polylines.add(tripResult['polyline']);
|
||||
|
||||
// 🔥 تخزين استجابة السيرفر كاملة (بما فيها الـ points والـ instructions)
|
||||
if (tripResult['raw_response'] != null) {
|
||||
box.write('cached_trip_route', tripResult['raw_response']);
|
||||
}
|
||||
}
|
||||
|
||||
await _updateMarkers(
|
||||
paxTime: timeToPassenger,
|
||||
paxDist: distanceToPassenger,
|
||||
destTime: totalTripDuration,
|
||||
destDist: totalTripDistance);
|
||||
|
||||
// Now zoom to fit all polylines and markers (if controller available)
|
||||
if (canZoom) {
|
||||
zoomToFitRide();
|
||||
}
|
||||
|
||||
update();
|
||||
} catch (e) {
|
||||
print("❌ Error in Journey Calculation: $e");
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDistance(dynamic rawDist) {
|
||||
if (rawDist == null || rawDist.toString().isEmpty) return "--";
|
||||
double dist = double.tryParse(rawDist.toString()) ?? 0.0;
|
||||
if (dist <= 0) return "--";
|
||||
if (dist < 1000) return "${dist.toStringAsFixed(0)} m";
|
||||
return "${(dist / 1000).toStringAsFixed(1)} km";
|
||||
}
|
||||
|
||||
String _formatDuration(dynamic rawDur) {
|
||||
if (rawDur == null || rawDur.toString().isEmpty) return "--";
|
||||
double dur = double.tryParse(rawDur.toString()) ?? 0.0;
|
||||
if (dur <= 0) return "1 min"; // Minimum 1 min for UI
|
||||
if (dur < 60) return "${dur.toStringAsFixed(0)} sec";
|
||||
return "${(dur / 60).toStringAsFixed(0)} min";
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> _fetchRouteData(
|
||||
{required LatLng start,
|
||||
required LatLng end,
|
||||
required Color color,
|
||||
required String id,
|
||||
bool getSteps = false}) async {
|
||||
try {
|
||||
if (start.latitude == 0 || end.latitude == 0) return null;
|
||||
// Don't block on mapController — route data fetch is independent
|
||||
|
||||
final saasUrl = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: {
|
||||
'fromLat': start.latitude.toString(),
|
||||
'fromLng': start.longitude.toString(),
|
||||
'toLat': end.latitude.toString(),
|
||||
'toLng': end.longitude.toString(),
|
||||
'steps': getSteps ? 'true' : 'false',
|
||||
'alternatives': 'false',
|
||||
'locale': 'ar',
|
||||
});
|
||||
|
||||
final response = await http.get(saasUrl, headers: {
|
||||
'x-api-key': Env.mapSaasKey,
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Routing request failed: ${response.statusCode}");
|
||||
}
|
||||
|
||||
final data = jsonDecode(response.body);
|
||||
print("🛣️ Route API Response [$id]: ${data}");
|
||||
|
||||
// The map-saas API returns the route data directly at the root,
|
||||
// with 'points' being an encoded polyline string.
|
||||
final String? encodedPoints = data['points']?.toString();
|
||||
|
||||
if (encodedPoints != null && encodedPoints.isNotEmpty) {
|
||||
List<LatLng> path = controllerDecodePolyline(encodedPoints);
|
||||
print("📍 Path for [$id] has ${path.length} points.");
|
||||
|
||||
final num? rawDist = data['distance'] is num ? data['distance'] : null;
|
||||
final num? rawDur = data['duration'] is num ? data['duration'] : null;
|
||||
|
||||
final distanceText = data['distance_text'] ?? _formatDistance(rawDist);
|
||||
final durationText = data['duration_text'] ?? _formatDuration(rawDur);
|
||||
|
||||
Polyline polyline = Polyline(
|
||||
polylineId: PolylineId(id),
|
||||
color: color,
|
||||
width: 5,
|
||||
points: path,
|
||||
);
|
||||
|
||||
return {
|
||||
'distance_text': distanceText,
|
||||
'duration_text': durationText,
|
||||
'polyline': polyline,
|
||||
'encoded_polyline': encodedPoints,
|
||||
'raw_response': response.body, // 🔥 نمرر الـ JSON كاملاً
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
print("Route Fetch Error: $e");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void zoomToFitRide() {
|
||||
if (mapController == null) return;
|
||||
|
||||
List<LatLng> allPoints = [];
|
||||
|
||||
// Add all polyline points to the bounds calculation
|
||||
for (var polyline in polylines) {
|
||||
allPoints.addAll(polyline.points);
|
||||
}
|
||||
|
||||
// Fallback to basic markers if polylines are empty
|
||||
if (allPoints.isEmpty) {
|
||||
allPoints.addAll([
|
||||
LatLng(latPassenger, lngPassenger),
|
||||
LatLng(latDestination, lngDestination),
|
||||
]);
|
||||
}
|
||||
|
||||
if (allPoints.isEmpty) return;
|
||||
|
||||
double minLat = allPoints.first.latitude;
|
||||
double maxLat = allPoints.first.latitude;
|
||||
double minLng = allPoints.first.longitude;
|
||||
double maxLng = allPoints.first.longitude;
|
||||
|
||||
for (var p in allPoints) {
|
||||
if (p.latitude < minLat) minLat = p.latitude;
|
||||
if (p.latitude > maxLat) maxLat = p.latitude;
|
||||
if (p.longitude < minLng) minLng = p.longitude;
|
||||
if (p.longitude > maxLng) maxLng = p.longitude;
|
||||
}
|
||||
|
||||
// Add some padding to the bounds
|
||||
double latPad = (maxLat - minLat) * 0.25;
|
||||
double lngPad = (maxLng - minLng) * 0.2;
|
||||
|
||||
mapController!.animateCamera(CameraUpdate.newLatLngBounds(
|
||||
LatLngBounds(
|
||||
southwest: LatLng(minLat - latPad, minLng - lngPad),
|
||||
northeast: LatLng(maxLat + latPad, maxLng + lngPad),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Markers & Setup
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
Future<void> _prepareDriverIcon() async {
|
||||
driverIcon = await MarkerGenerator.createDriverMarker();
|
||||
}
|
||||
|
||||
Future<void> _updateMarkers(
|
||||
{required String paxTime,
|
||||
required String paxDist,
|
||||
String? destTime,
|
||||
String? destDist}) async {
|
||||
// حماية إذا لم يتم جلب الإحداثيات
|
||||
if (latPassenger == 0 || latDestination == 0) return;
|
||||
|
||||
final InlqBitmap pickupIcon =
|
||||
await MarkerGenerator.createCustomMarkerBitmap(
|
||||
title: paxTime,
|
||||
subtitle: paxDist,
|
||||
color: Colors.amber.shade900, // Matching the amber pickup line
|
||||
iconData: Icons.person_pin_circle,
|
||||
);
|
||||
|
||||
final InlqBitmap dropoffIcon =
|
||||
await MarkerGenerator.createCustomMarkerBitmap(
|
||||
title: destTime ?? totalTripDuration,
|
||||
subtitle: destDist ?? totalTripDistance,
|
||||
color: Colors.red.shade800,
|
||||
iconData: Icons.flag,
|
||||
);
|
||||
|
||||
markersMap[const MarkerId('pax')] = Marker(
|
||||
markerId: const MarkerId('pax'),
|
||||
position: LatLng(latPassenger, lngPassenger),
|
||||
icon: pickupIcon,
|
||||
anchor: const Offset(0.5, 0.85),
|
||||
);
|
||||
|
||||
markersMap[const MarkerId('dest')] = Marker(
|
||||
markerId: const MarkerId('dest'),
|
||||
position: LatLng(latDestination, lngDestination),
|
||||
icon: dropoffIcon,
|
||||
anchor: const Offset(0.5, 0.85),
|
||||
);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void _initialMapSetup() async {
|
||||
Position driverPos = await Geolocator.getCurrentPosition();
|
||||
LatLng driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
|
||||
|
||||
if (driverIcon != null) {
|
||||
markersMap[const MarkerId('driver')] = Marker(
|
||||
markerId: const MarkerId('driver'),
|
||||
position: driverLatLng,
|
||||
icon: driverIcon!,
|
||||
rotation: driverPos.heading,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
flat: true,
|
||||
zIndex: 10);
|
||||
}
|
||||
|
||||
if (latPassenger != 0 && lngPassenger != 0) {
|
||||
polylines.add(Polyline(
|
||||
polylineId: const PolylineId('temp_line'),
|
||||
points: [driverLatLng, LatLng(latPassenger, lngPassenger)],
|
||||
color: Colors.grey,
|
||||
width: 2,
|
||||
));
|
||||
|
||||
zoomToFitRide();
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void updateDriverLocation(LatLng newPos, double heading) {
|
||||
if (driverIcon != null) {
|
||||
markersMap[const MarkerId('driver')] = Marker(
|
||||
markerId: const MarkerId('driver'),
|
||||
position: newPos,
|
||||
icon: driverIcon!,
|
||||
rotation: heading,
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
flat: true,
|
||||
zIndex: 10,
|
||||
);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void onMapCreated(IntaleqMapController controller) {
|
||||
mapController = controller;
|
||||
_calculateFullJourney();
|
||||
}
|
||||
|
||||
// --- قبول الطلب وإدارة التايمر ---
|
||||
void startTimer() {
|
||||
_timer?.cancel();
|
||||
remainingTime = duration;
|
||||
_playAudio();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (remainingTime <= 0) {
|
||||
timer.cancel();
|
||||
_stopAudio();
|
||||
if (!applied) Get.back();
|
||||
} else {
|
||||
remainingTime--;
|
||||
progress = remainingTime / duration;
|
||||
update();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void endTimer() => _timer?.cancel();
|
||||
void changeApplied() => applied = true;
|
||||
|
||||
void _playAudio() async {
|
||||
try {
|
||||
await audioPlayer.setAsset('assets/order.mp3', preload: true);
|
||||
await audioPlayer.setLoopMode(LoopMode.one);
|
||||
await audioPlayer.play();
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
|
||||
void _stopAudio() => audioPlayer.stop();
|
||||
|
||||
void _listenForRideTaken() {
|
||||
if (locationController.socket != null) {
|
||||
locationController.socket!.off('ride_taken');
|
||||
locationController.socket!.on('ride_taken', (data) {
|
||||
if (_isRideTakenHandled) return;
|
||||
String takenRideId = data['ride_id'].toString();
|
||||
String myCurrentRideId = _safeGet(16);
|
||||
String whoTookIt = data['taken_by_driver_id'].toString();
|
||||
String myDriverId = box.read(BoxName.driverID).toString();
|
||||
|
||||
if (takenRideId == myCurrentRideId && whoTookIt != myDriverId) {
|
||||
_isRideTakenHandled = true;
|
||||
endTimer();
|
||||
// 1. حذف الإشعار من شريط التنبيهات فوراً
|
||||
NotificationController().cancelOrderNotification();
|
||||
if (Get.isSnackbarOpen) Get.closeCurrentSnackbar();
|
||||
|
||||
// إغلاق أي ديالوج مفتوح قسرياً
|
||||
if (Get.isDialogOpen ?? false) {
|
||||
navigatorKey.currentState?.pop();
|
||||
}
|
||||
Get.back();
|
||||
mySnackbarInfo("The order has been accepted by another driver.".tr);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.detached) {
|
||||
isInBackground = true;
|
||||
} else if (state == AppLifecycleState.resumed) {
|
||||
isInBackground = false;
|
||||
FlutterOverlayWindow.closeOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
void _checkOverlay() async {
|
||||
if (Platform.isAndroid && await FlutterOverlayWindow.isActive()) {
|
||||
await FlutterOverlayWindow.closeOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// Accept Order Logic
|
||||
Future<void> acceptOrder() async {
|
||||
endTimer();
|
||||
_stopAudio();
|
||||
|
||||
// 1. إرسال الطلب
|
||||
var res = await CRUD()
|
||||
.post(link: "${AppLink.ride}/rides/acceptRide.php", payload: {
|
||||
'id': _safeGet(16),
|
||||
'rideTimeStart': DateTime.now().toString(),
|
||||
'status': 'Apply',
|
||||
'passengerToken': _safeGet(9),
|
||||
'driver_id': box.read(BoxName.driverID),
|
||||
});
|
||||
|
||||
Log.print('res from orderrequestpage: ${res}');
|
||||
|
||||
// ============================================================
|
||||
// تصحيح: فحص الرد بدقة (Map أو String)
|
||||
// ============================================================
|
||||
bool isFailure = false;
|
||||
|
||||
if (res is Map && res['status'] == 'failure') {
|
||||
isFailure = true;
|
||||
} else if (res == 'failure') {
|
||||
isFailure = true;
|
||||
}
|
||||
|
||||
if (isFailure) {
|
||||
// ⛔ حالة الفشل: الطلب مأخوذ
|
||||
MyDialog().getDialog(
|
||||
"Sorry, the order was taken by another driver.".tr, '', () {
|
||||
// بما أن MyDialog يغلق نفسه الآن، نحتاج Get.back() واحدة فقط لإغلاق صفحة الطلب
|
||||
Get.back();
|
||||
});
|
||||
} else {
|
||||
// ✅ حالة النجاح
|
||||
|
||||
// حماية من الكراش: التأكد من وجود HomeCaptainController قبل استخدامه
|
||||
if (!Get.isRegistered<HomeCaptainController>()) {
|
||||
Get.put(HomeCaptainController());
|
||||
} else {
|
||||
Get.find<HomeCaptainController>().changeRideId();
|
||||
}
|
||||
|
||||
box.write(BoxName.statusDriverLocation, 'on');
|
||||
changeApplied();
|
||||
|
||||
var rideArgs = {
|
||||
'passengerLocation': '${_safeGet(0)},${_safeGet(1)}',
|
||||
'passengerDestination': '${_safeGet(3)},${_safeGet(4)}',
|
||||
'Duration': totalTripDuration,
|
||||
'totalCost': _safeGet(26),
|
||||
'Distance': totalTripDistance,
|
||||
'name': _safeGet(8),
|
||||
'phone': _safeGet(10),
|
||||
'email': _safeGet(28),
|
||||
'WalletChecked': _safeGet(13),
|
||||
'tokenPassenger': _safeGet(9),
|
||||
'direction':
|
||||
'https://www.google.com/maps/dir/${_safeGet(0)}/${_safeGet(1)}/',
|
||||
'DurationToPassenger': timeToPassenger,
|
||||
'rideId': _safeGet(16),
|
||||
'passengerId': _safeGet(7),
|
||||
'driverId': _safeGet(18),
|
||||
'durationOfRideValue': totalTripDuration,
|
||||
'paymentAmount': _safeGet(2),
|
||||
'paymentMethod': _safeGet(13) == 'true' ? 'visa' : 'cash',
|
||||
'isHaveSteps': _safeGet(20),
|
||||
'step0': _safeGet(21),
|
||||
'step1': _safeGet(22),
|
||||
'step2': _safeGet(23),
|
||||
'step3': _safeGet(24),
|
||||
'step4': _safeGet(25),
|
||||
'passengerWalletBurc': _safeGet(26),
|
||||
'timeOfOrder': DateTime.now().toString(),
|
||||
'totalPassenger': _safeGet(2),
|
||||
'carType': _safeGet(31),
|
||||
'kazan': _safeGet(32),
|
||||
'startNameLocation': _safeGet(29),
|
||||
'endNameLocation': _safeGet(30),
|
||||
};
|
||||
|
||||
box.write(BoxName.rideArguments, rideArgs);
|
||||
|
||||
// الانتقال النهائي
|
||||
Get.off(() => PassengerLocationMapPage(), arguments: rideArgs);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
locationController.socket?.off('ride_taken');
|
||||
audioPlayer.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_timer?.cancel();
|
||||
// mapController?.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
List<LatLng> controllerDecodePolyline(String encoded) {
|
||||
List<LatLng> points = [];
|
||||
int index = 0, len = encoded.length;
|
||||
int lat = 0, lng = 0;
|
||||
|
||||
while (index < len) {
|
||||
int b, shift = 0, result = 0;
|
||||
do {
|
||||
b = encoded.codeUnitAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
|
||||
lat += dlat;
|
||||
|
||||
shift = 0;
|
||||
result = 0;
|
||||
do {
|
||||
b = encoded.codeUnitAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
|
||||
lng += dlng;
|
||||
|
||||
points.add(LatLng(lat / 1E5, lng / 1E5));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
}
|
||||
212
siro_driver/lib/controller/home/captin/v2_review_delta.html
Normal file
212
siro_driver/lib/controller/home/captin/v2_review_delta.html
Normal file
@@ -0,0 +1,212 @@
|
||||
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
.wrap { padding: 1.25rem 1rem; font-size: 14px; color: var(--color-text-primary); direction: rtl; }
|
||||
h1 { font-size: 18px; font-weight: 500; margin: 0 0 3px; }
|
||||
.sub { font-size: 13px; color: var(--color-text-secondary); margin: 0 0 1.25rem; }
|
||||
.badge { display: inline-flex; align-items: center; font-size: 11px; font-weight: 500; padding: 2px 8px; border-radius: 20px; white-space: nowrap; }
|
||||
.b-ok { background: var(--color-background-success); color: var(--color-text-success); }
|
||||
.b-new { background: var(--color-background-danger); color: var(--color-text-danger); }
|
||||
.b-med { background: var(--color-background-warning); color: var(--color-text-warning); }
|
||||
.b-min { background: var(--color-background-info); color: var(--color-text-info); }
|
||||
.progress-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 1.25rem; }
|
||||
.pcard { background: var(--color-background-secondary); border-radius: var(--border-radius-md); padding: 12px 14px; }
|
||||
.pcard .val { font-size: 28px; font-weight: 500; }
|
||||
.pcard .lbl { font-size: 12px; color: var(--color-text-secondary); margin-top: 2px; }
|
||||
.ok-val { color: var(--color-text-success); }
|
||||
.bad-val { color: var(--color-text-danger); }
|
||||
.new-val { color: var(--color-text-warning); }
|
||||
.section { margin-bottom: 1.4rem; }
|
||||
.section-hdr { font-size: 14px; font-weight: 500; margin: 0 0 8px; display: flex; align-items: center; gap: 8px; }
|
||||
.card { background: var(--color-background-primary); border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-lg); margin-bottom: 8px; overflow: hidden; }
|
||||
.card.fixed { border-right: 3px solid var(--color-border-success); }
|
||||
.card.broken{ border-right: 3px solid var(--color-border-danger); }
|
||||
.card.new { border-right: 3px solid var(--color-border-warning); }
|
||||
.card.minor { border-right: 3px solid var(--color-border-info); }
|
||||
.ch { display: flex; align-items: flex-start; gap: 8px; padding: 10px 14px; cursor: pointer; }
|
||||
.ch:hover { background: var(--color-background-secondary); }
|
||||
.ch-icon { font-size: 15px; flex-shrink: 0; margin-top: 1px; }
|
||||
.ch-title { font-size: 13.5px; font-weight: 500; flex: 1; line-height: 1.4; }
|
||||
.ch-badge { flex-shrink: 0; }
|
||||
.chev { font-size: 11px; color: var(--color-text-tertiary); transition: transform .2s; margin-right: auto; margin-left: 4px; }
|
||||
.chev.open { transform: rotate(90deg); }
|
||||
.cb { display: none; padding: 0 14px 14px; border-top: 0.5px solid var(--color-border-tertiary); }
|
||||
.cb.open { display: block; }
|
||||
.cb p { font-size: 13px; color: var(--color-text-secondary); line-height: 1.7; margin: 8px 0 6px; }
|
||||
pre { font-family: var(--font-mono); font-size: 11.5px; background: var(--color-background-tertiary); border: 0.5px solid var(--color-border-tertiary); border-radius: var(--border-radius-md); padding: 9px 11px; overflow-x: auto; margin: 6px 0; line-height: 1.6; white-space: pre; }
|
||||
.fix { background: var(--color-background-success); border-radius: var(--border-radius-md); padding: 8px 11px; margin-top: 8px; font-size: 13px; line-height: 1.6; }
|
||||
.fix strong { color: var(--color-text-success); font-size: 11px; display: block; margin-bottom: 2px; }
|
||||
.warn { background: var(--color-background-warning); border-radius: var(--border-radius-md); padding: 8px 11px; margin-top: 8px; font-size: 13px; line-height: 1.6; }
|
||||
.warn strong { color: var(--color-text-warning); font-size: 11px; display: block; margin-bottom: 2px; }
|
||||
code { font-family: var(--font-mono); font-size: 12px; background: var(--color-background-secondary); padding: 0 4px; border-radius: 3px; }
|
||||
.score-row { display: flex; align-items: center; gap: 10px; font-size: 13px; margin-bottom: 7px; }
|
||||
.score-lbl { min-width: 160px; color: var(--color-text-secondary); }
|
||||
.strack { flex: 1; height: 6px; background: var(--color-border-tertiary); border-radius: 3px; position: relative; }
|
||||
.sfill { height: 100%; border-radius: 3px; }
|
||||
.sval { min-width: 36px; font-size: 12px; color: var(--color-text-secondary); text-align: left; }
|
||||
</style>
|
||||
|
||||
<div class="wrap">
|
||||
<h1>مراجعة النسخة المحدّثة — V2</h1>
|
||||
<p class="sub">مقارنة مع المراجعة السابقة · 16 مشكلة فُحصت</p>
|
||||
|
||||
<div class="progress-row">
|
||||
<div class="pcard"><div class="val ok-val">11</div><div class="lbl">مشكلة مُصلحة ✅</div></div>
|
||||
<div class="pcard"><div class="val bad-val">2</div><div class="lbl">مشكلة جديدة أدخلتها الإصلاحات ⚠️</div></div>
|
||||
<div class="pcard"><div class="val new-val">3</div><div class="lbl">مشكلة لم تُعالج بعد</div></div>
|
||||
<div class="pcard"><div class="val ok-val">69%</div><div class="lbl">تحسن من المراجعة الأولى</div></div>
|
||||
</div>
|
||||
|
||||
<div class="score-row"><span class="score-lbl">صحة المنطق البرمجي</span><div class="strack"><div class="sfill" style="width:72%;background:#3B8BD4"></div></div><span class="sval">72% ↑</span></div>
|
||||
<div class="score-row"><span class="score-lbl">نظافة الكود</span><div class="strack"><div class="sfill" style="width:63%;background:#1D9E75"></div></div><span class="sval">63% ↑</span></div>
|
||||
<div class="score-row" style="margin-bottom:1.4rem"><span class="score-lbl">قابلية الصيانة</span><div class="strack"><div class="sfill" style="width:58%;background:#1D9E75"></div></div><span class="sval">58% ↑</span></div>
|
||||
|
||||
<!-- FIXED -->
|
||||
<div class="section">
|
||||
<div class="section-hdr"><span class="badge b-ok">✅ مُصلح</span> ما تم إصلاحه بشكل صحيح</div>
|
||||
|
||||
<div class="card fixed">
|
||||
<div class="ch" onclick="t(this)"><span class="ch-icon">✅</span><span class="ch-title">C-1 — استبدال الحلقة التكرارية والاستدعاء الذاتي بـ <code>Timer.periodic</code></span><span class="ch-badge badge b-ok">ممتاز</span><span class="chev">▶</span></div>
|
||||
<div class="cb"><p>تم حذف <code>updateLocation()</code> كاملاً واستبدالها بـ <code>startUpdateLocationTimer()</code> و <code>stopUpdateLocationTimer()</code>. التايمر مسجّل في <code>onClose()</code> و <code>_stopAllServices()</code>. إصلاح ممتاز.</p>
|
||||
<div class="warn"><strong>ملاحظة مهمة</strong>لا يظهر في الكود استدعاء لـ <code>startUpdateLocationTimer()</code> من أي مكان. يجب التأكد أنها تُستدعى من الـ View أو من <code>startRideFromDriver()</code>.</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card fixed">
|
||||
<div class="ch" onclick="t(this)"><span class="ch-icon">✅</span><span class="ch-title">C-4 — تحديث <code>myLocation</code> في <code>_handleLocationUpdate()</code></span><span class="chev">▶</span></div>
|
||||
<div class="cb"><pre>void _handleLocationUpdate(geo.Position pos) {
|
||||
final newLoc = LatLng(pos.latitude, pos.longitude);
|
||||
myLocation = newLoc; // ← [Fix C-4] ✅ صحيح
|
||||
// ...</pre></div>
|
||||
</div>
|
||||
|
||||
<div class="card fixed">
|
||||
<div class="ch" onclick="t(this)"><span class="ch-icon">✅</span><span class="ch-title">M-4 — دمج <code>checkForNextStep()</code> مع <code>_checkNavigationStep()</code></span><span class="chev">▶</span></div>
|
||||
<div class="cb"><p><code>checkForNextStep</code> أصبحت wrapper بسيط يستدعي <code>_checkNavigationStep</code>. منطق واحد، لا تعارض.</p></div>
|
||||
</div>
|
||||
|
||||
<div class="card fixed">
|
||||
<div class="ch" onclick="t(this)"><span class="ch-icon">✅</span><span class="ch-title">M-5 — <code>disposeEverything()</code> لا تستدعي <code>onClose()</code> يدوياً</span><span class="chev">▶</span></div>
|
||||
<div class="cb"><pre>void disposeEverything() {
|
||||
_stopAllServices(); // ✅ بدون onClose()
|
||||
}</pre></div>
|
||||
</div>
|
||||
|
||||
<div class="card fixed">
|
||||
<div class="ch" onclick="t(this)"><span class="ch-icon">✅</span><span class="ch-title">C-3 جزئي — دالة مساعدة <code>_parseDistanceToMeters()</code> مشتركة</span><span class="chev">▶</span></div>
|
||||
<div class="cb"><p>تم استخراج منطق تحليل المسافة إلى دالة واحدة تستخدمها كلا <code>finishRideFromDriver()</code> و <code>_validateTripDistance()</code>. يحل مشكلة التضارب في الوحدات.</p>
|
||||
<div class="warn"><strong>لم يُحل كاملاً</strong>التحقق من المسافة لا يزال يحدث مرتين (انظر مشكلة C-3 أدناه).</div></div>
|
||||
</div>
|
||||
|
||||
<div class="card fixed">
|
||||
<div class="ch" onclick="t(this)"><span class="ch-icon">✅</span><span class="ch-title">M-1 + M-2 + M-6 + N-1 + N-5 — إصلاحات طفيفة متعددة</span><span class="chev">▶</span></div>
|
||||
<div class="cb">
|
||||
<p><strong>M-1:</strong> <code>jitterMeters</code> → <code>jitterKm = 0.01</code> ✅</p>
|
||||
<p><strong>M-2:</strong> <code>distance</code> المحلية → <code>distToPassenger</code> ✅</p>
|
||||
<p><strong>M-6:</strong> تعليق يوضح أن الوحدة كيلومتر ✅</p>
|
||||
<p><strong>N-1:</strong> <code>&directionsmode</code> → <code>?directionsmode</code> ✅</p>
|
||||
<p><strong>N-5:</strong> إضافة <code>update()</code> في <code>getLocationArea()</code> ✅</p>
|
||||
<p><strong>M-3:</strong> حذف <code>_performanceReadings</code> والمتغيرات الميتة ✅</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NEW BUGS -->
|
||||
<div class="section">
|
||||
<div class="section-hdr"><span class="badge b-new">🚨 جديد</span> مشاكل أدخلتها الإصلاحات</div>
|
||||
|
||||
<div class="card new">
|
||||
<div class="ch" onclick="t(this)"><span class="ch-icon">🚨</span><span class="ch-title">BUG جديد — <code>Completer</code> في C-2 يُسبب Deadlock عند إغلاق الديالوج بـ Back</span><span class="ch-badge badge b-new">حرج</span><span class="chev">▶</span></div>
|
||||
<div class="cb">
|
||||
<p>الإصلاح استخدم <code>Completer</code> بشكل صحيح لحل مشكلة الـ callback الآني، لكنه أدخل مشكلة أخرى: لو أغلق المستخدم الديالوج بزر الرجوع (Back) في Android بدون ضغط OK، فإن <code>completer.future</code> لن تكتمل أبداً، والدالة ستبقى معلّقة (deadlock) لأن <code>_validateTripDistance()</code> هي <code>async</code> وتنتظر نتيجة لن تأتي:</p>
|
||||
<pre>final completer = Completer<bool>();
|
||||
MyDialog().getDialog('Exit Ride?'.tr, '', () {
|
||||
if (!completer.isCompleted) completer.complete(true);
|
||||
Get.back();
|
||||
});
|
||||
return await completer.future; // ← ينتظر للأبد إذا أُغلق بـ Back</pre>
|
||||
<div class="fix"><strong>الحل</strong>أضف <code>barrierDismissible: false</code> للديالوج، أو استخدم <code>completer.complete(false)</code> عند إغلاق الديالوج بدون تأكيد (عبر <code>WillPopScope</code> أو <code>onDismissed</code> callback في <code>MyDialog</code>).</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card new">
|
||||
<div class="ch" onclick="t(this)"><span class="ch-icon">🚨</span><span class="ch-title">C-3 لا يزال — المستخدم يرى ديالوجَي تأكيد متتاليَين عند إنهاء الرحلة بالزر</span><span class="ch-badge badge b-new">حرج</span><span class="chev">▶</span></div>
|
||||
<div class="cb">
|
||||
<p>رغم إضافة <code>_parseDistanceToMeters()</code>، تدفق الكود لا يزال يُقدّم ديالوجَين:</p>
|
||||
<pre>// finishRideFromDriver(isFromSlider: false):
|
||||
MyDialog().getDialog('Are you sure to exit ride?', '', () {
|
||||
Get.back();
|
||||
finishRideFromDriver1(); // ← isFromSlider = false افتراضياً
|
||||
});
|
||||
|
||||
// finishRideFromDriver1():
|
||||
if (!await _validateTripDistance(false)) return; // ← يُقدّم ديالوجاً ثانياً!</pre>
|
||||
<p>المستخدم يرى "هل أنت متأكد؟" → يضغط OK → يرى "Exit Ride?" مرة ثانية → ينتظر مجدداً.</p>
|
||||
<div class="fix"><strong>الحل</strong>احذف الديالوج من <code>finishRideFromDriver()</code> وأبقه في <code>_validateTripDistance()</code> فقط. أو مرّر <code>isFromSlider: true</code> لما يأتي من موافقة مسبقة.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REMAINING -->
|
||||
<div class="section">
|
||||
<div class="section-hdr"><span class="badge b-med">⚠️ لم تُعالج</span> مشاكل لا تزال قائمة</div>
|
||||
|
||||
<div class="card broken">
|
||||
<div class="ch" onclick="t(this)"><span class="ch-icon">⚠️</span><span class="ch-title">M-7 — Null checks على <code>String</code> غير قابلة للـ null</span><span class="chev">▶</span></div>
|
||||
<div class="cb">
|
||||
<pre>if (isSocialPressed == true && passengerId != null && rideId != null) {
|
||||
// ^^^^^^^^^^^ دائماً non-null</pre>
|
||||
<p>لو <code>passengerId == ''</code> يمر الشرط ويُرسل بيانات فارغة للسيرفر. الفحص الصحيح: <code>passengerId.isNotEmpty && rideId.isNotEmpty</code>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card broken">
|
||||
<div class="ch" onclick="t(this)"><span class="ch-icon">⚠️</span><span class="ch-title">N-2 — تأخير 1 ثانية Hardcoded في <code>argumentLoading()</code></span><span class="chev">▶</span></div>
|
||||
<div class="cb">
|
||||
<pre>await Future.delayed(const Duration(seconds: 1));
|
||||
await getRoute(...);</pre>
|
||||
<p>لا يزال موجوداً. Race condition يجب معالجته بـ <code>Completer</code> بدلاً من تخمين الوقت.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card broken">
|
||||
<div class="ch" onclick="t(this)"><span class="ch-icon">⚠️</span><span class="ch-title">N-4 — <code>step0</code> إلى <code>step4</code> بدلاً من <code>List<String></code></span><span class="chev">▶</span></div>
|
||||
<div class="cb">
|
||||
<pre>String step0 = ''; String step1 = ''; // ...
|
||||
step0 = Get.arguments['step0']?.toString() ?? '';
|
||||
step1 = Get.arguments['step1']?.toString() ?? '';</pre>
|
||||
<p>لا تزال 5 متغيرات منفصلة. <code>List<String> steps = List.filled(5, '')</code> أوضح وأسهل في المعالجة.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STILL MINOR -->
|
||||
<div class="section">
|
||||
<div class="section-hdr"><span class="badge b-min">ℹ️ بسيطة</span> ملاحظات إضافية على هذه النسخة</div>
|
||||
|
||||
<div class="card minor">
|
||||
<div class="ch" onclick="t(this)"><span class="ch-icon">ℹ️</span><span class="ch-title"><code>_suggestOptimization()</code> لا تزال موجودة لكن لا يستدعيها أحد</span><span class="chev">▶</span></div>
|
||||
<div class="cb"><p>بعد حذف <code>_performanceReadings</code> و <code>_analyzePerformance()</code>، بقيت <code>_suggestOptimization()</code> معزولة. إما أن تُستدعى من مكان ما أو تُحذف.</p></div>
|
||||
</div>
|
||||
|
||||
<div class="card minor">
|
||||
<div class="ch" onclick="t(this)"><span class="ch-icon">ℹ️</span><span class="ch-title">الاستيرادات المكررة لـ <code>dart:math</code> و <code>geolocator</code> لا تزال</span><span class="chev">▶</span></div>
|
||||
<div class="cb">
|
||||
<pre>import 'dart:math';
|
||||
import 'dart:math' as math; // مكرر
|
||||
import 'package:geolocator/geolocator.dart' as geo;
|
||||
import 'package:geolocator/geolocator.dart'; // مكرر</pre>
|
||||
<p>يُسبب تحذيرات من المحلل ويُشوّش قراءة الكود. احذف النسخة غير المعرّفة.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
function t(header) {
|
||||
const b = header.nextElementSibling;
|
||||
const ch = header.querySelector('.chev');
|
||||
const o = b.classList.contains('open');
|
||||
b.classList.toggle('open', !o);
|
||||
if (ch) ch.classList.toggle('open', !o);
|
||||
}
|
||||
</script>
|
||||
106
siro_driver/lib/controller/home/journal/schedule_controller.dart
Normal file
106
siro_driver/lib/controller/home/journal/schedule_controller.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
|
||||
class WorkSlot {
|
||||
int dayOfWeek; // 1=Mon ... 7=Sun
|
||||
TimeOfDay startTime;
|
||||
TimeOfDay endTime;
|
||||
bool isActive;
|
||||
|
||||
WorkSlot({required this.dayOfWeek, required this.startTime, required this.endTime, this.isActive = true});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'day': dayOfWeek, 'startH': startTime.hour, 'startM': startTime.minute,
|
||||
'endH': endTime.hour, 'endM': endTime.minute, 'active': isActive,
|
||||
};
|
||||
|
||||
factory WorkSlot.fromJson(Map<String, dynamic> json) => WorkSlot(
|
||||
dayOfWeek: json['day'] ?? 1,
|
||||
startTime: TimeOfDay(hour: json['startH'] ?? 8, minute: json['startM'] ?? 0),
|
||||
endTime: TimeOfDay(hour: json['endH'] ?? 17, minute: json['endM'] ?? 0),
|
||||
isActive: json['active'] ?? true,
|
||||
);
|
||||
|
||||
String get dayName {
|
||||
const days = ['', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
return days[dayOfWeek];
|
||||
}
|
||||
|
||||
String get dayNameAr {
|
||||
const days = ['', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت', 'الأحد'];
|
||||
return days[dayOfWeek];
|
||||
}
|
||||
|
||||
String formatTime(TimeOfDay t) => '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
|
||||
String get timeRange => '${formatTime(startTime)} - ${formatTime(endTime)}';
|
||||
}
|
||||
|
||||
class ScheduleController extends GetxController {
|
||||
List<WorkSlot> schedule = [];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_loadSchedule();
|
||||
}
|
||||
|
||||
void _loadSchedule() {
|
||||
final saved = box.read('work_schedule');
|
||||
if (saved != null) {
|
||||
try {
|
||||
final list = jsonDecode(saved) as List;
|
||||
schedule = list.map((e) => WorkSlot.fromJson(e)).toList();
|
||||
} catch (_) {
|
||||
_initDefault();
|
||||
}
|
||||
} else {
|
||||
_initDefault();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void _initDefault() {
|
||||
schedule = List.generate(7, (i) => WorkSlot(
|
||||
dayOfWeek: i + 1,
|
||||
startTime: const TimeOfDay(hour: 8, minute: 0),
|
||||
endTime: const TimeOfDay(hour: 18, minute: 0),
|
||||
isActive: i < 6, // الجمعة عطلة
|
||||
));
|
||||
}
|
||||
|
||||
void _save() {
|
||||
box.write('work_schedule', jsonEncode(schedule.map((s) => s.toJson()).toList()));
|
||||
update();
|
||||
}
|
||||
|
||||
void toggleDay(int dayOfWeek) {
|
||||
final slot = schedule.firstWhere((s) => s.dayOfWeek == dayOfWeek);
|
||||
slot.isActive = !slot.isActive;
|
||||
_save();
|
||||
}
|
||||
|
||||
void updateStartTime(int dayOfWeek, TimeOfDay time) {
|
||||
schedule.firstWhere((s) => s.dayOfWeek == dayOfWeek).startTime = time;
|
||||
_save();
|
||||
}
|
||||
|
||||
void updateEndTime(int dayOfWeek, TimeOfDay time) {
|
||||
schedule.firstWhere((s) => s.dayOfWeek == dayOfWeek).endTime = time;
|
||||
_save();
|
||||
}
|
||||
|
||||
double get totalWeeklyHours {
|
||||
double total = 0;
|
||||
for (var s in schedule) {
|
||||
if (!s.isActive) continue;
|
||||
final startMin = s.startTime.hour * 60 + s.startTime.minute;
|
||||
final endMin = s.endTime.hour * 60 + s.endTime.minute;
|
||||
total += (endMin - startMin) / 60;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
int get activeDays => schedule.where((s) => s.isActive).length;
|
||||
}
|
||||
14
siro_driver/lib/controller/home/menu_controller.dart
Executable file
14
siro_driver/lib/controller/home/menu_controller.dart
Executable file
@@ -0,0 +1,14 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MyMenuController extends GetxController {
|
||||
bool isDrawerOpen = true;
|
||||
|
||||
void getDrawerMenu() {
|
||||
if (isDrawerOpen == true) {
|
||||
isDrawerOpen = false;
|
||||
} else {
|
||||
isDrawerOpen = true;
|
||||
}
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
List<LatLng> decodePolylineIsolate(String encoded) {
|
||||
List<LatLng> points = [];
|
||||
int index = 0, len = encoded.length;
|
||||
int lat = 0, lng = 0;
|
||||
|
||||
while (index < len) {
|
||||
int b, shift = 0, result = 0;
|
||||
do {
|
||||
b = encoded.codeUnitAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
|
||||
lat += dlat;
|
||||
|
||||
shift = 0;
|
||||
result = 0;
|
||||
do {
|
||||
b = encoded.codeUnitAt(index++) - 63;
|
||||
result |= (b & 0x1f) << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x20);
|
||||
int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
|
||||
lng += dlng;
|
||||
|
||||
points.add(LatLng(lat / 1E5, lng / 1E5));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
1154
siro_driver/lib/controller/home/navigation/navigation_view.dart
Normal file
1154
siro_driver/lib/controller/home/navigation/navigation_view.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,145 @@
|
||||
// lib/controllers/navigation/route_matcher_worker.dart
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'dart:typed_data';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'dart:math';
|
||||
|
||||
/// Worker entrypoint (spawnUri/spawn).
|
||||
/// Messages:
|
||||
/// - init: {'type':'init','coords': Float64List}
|
||||
/// - match: {'type':'match','id': int, 'lat': double, 'lng': double, 'lastIndex': int, 'window': int}
|
||||
/// - dispose: {'type':'dispose'}
|
||||
///
|
||||
/// Responses are sent back as Map via SendPort:
|
||||
/// - {'type':'ready'}
|
||||
/// - {'type':'matchResult','id': id, 'index': overallIndex, 'lat': lat, 'lng': lng, 'dist': meters}
|
||||
void routeMatcherIsolateEntry(SendPort sendPort) {
|
||||
final ReceivePort port = ReceivePort();
|
||||
sendPort.send({'type': 'ready', 'port': port.sendPort});
|
||||
|
||||
Float64List? flat; // [lat,lng,lat,lng,...]
|
||||
int nPoints = 0;
|
||||
|
||||
port.listen((dynamic message) {
|
||||
try {
|
||||
if (message is Map<String, dynamic>) {
|
||||
final type = message['type'] as String? ?? '';
|
||||
if (type == 'init') {
|
||||
final data = message['coords'] as Float64List?;
|
||||
if (data != null) {
|
||||
flat = data;
|
||||
nPoints = flat!.length ~/ 2;
|
||||
sendPort.send({'type': 'inited', 'points': nPoints});
|
||||
} else {
|
||||
sendPort.send({'type': 'error', 'message': 'init missing coords'});
|
||||
}
|
||||
} else if (type == 'match') {
|
||||
if (flat == null) {
|
||||
sendPort.send({'type': 'error', 'message': 'not inited'});
|
||||
return;
|
||||
}
|
||||
final int id = message['id'] as int;
|
||||
final double lat = (message['lat'] as num).toDouble();
|
||||
final double lng = (message['lng'] as num).toDouble();
|
||||
final int lastIndex = (message['lastIndex'] as int?) ?? 0;
|
||||
final int window = (message['window'] as int?) ?? 120;
|
||||
|
||||
final result =
|
||||
_findClosestWindowInternal(flat!, lat, lng, lastIndex, window);
|
||||
sendPort.send({
|
||||
'type': 'matchResult',
|
||||
'id': id,
|
||||
'index': result['index'],
|
||||
'lat': result['lat'],
|
||||
'lng': result['lng'],
|
||||
'dist': result['dist']
|
||||
});
|
||||
} else if (type == 'dispose') {
|
||||
port.close();
|
||||
sendPort.send({'type': 'disposed'});
|
||||
} else {
|
||||
sendPort.send({'type': 'error', 'message': 'unknown message type'});
|
||||
}
|
||||
}
|
||||
} catch (e, st) {
|
||||
sendPort.send(
|
||||
{'type': 'error', 'message': e.toString(), 'stack': st.toString()});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Internal helper: projection on segments, windowed search.
|
||||
/// Returns Map {index, lat, lng, dist}
|
||||
Map<String, dynamic> _findClosestWindowInternal(
|
||||
Float64List flat, double lat, double lng, int lastIndex, int window) {
|
||||
final int n = flat.length ~/ 2;
|
||||
final int start = max(0, lastIndex - window);
|
||||
final int end = min(n - 1, lastIndex + window);
|
||||
|
||||
double minDist = double.infinity;
|
||||
int bestIdx = lastIndex;
|
||||
double bestLat = flat[lastIndex * 2];
|
||||
double bestLng = flat[lastIndex * 2 + 1];
|
||||
|
||||
for (int i = start; i < end; i++) {
|
||||
final double aLat = flat[i * 2];
|
||||
final double aLng = flat[i * 2 + 1];
|
||||
final double bLat = flat[(i + 1) * 2];
|
||||
final double bLng = flat[(i + 1) * 2 + 1];
|
||||
|
||||
final proj = _closestPointOnSegmentLatLng(lat, lng, aLat, aLng, bLat, bLng);
|
||||
final double d = proj['dist'] as double;
|
||||
if (d < minDist) {
|
||||
minDist = d;
|
||||
bestLat = proj['lat'] as double;
|
||||
bestLng = proj['lng'] as double;
|
||||
// choose overall index: i or i+1 depending on t
|
||||
final double t = proj['t'] as double;
|
||||
bestIdx = i + (t > 0.5 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
return {'index': bestIdx, 'lat': bestLat, 'lng': bestLng, 'dist': minDist};
|
||||
}
|
||||
|
||||
/// Projection math on geodetic points approximated in degrees (good for short distances).
|
||||
Map<String, dynamic> _closestPointOnSegmentLatLng(
|
||||
double px, double py, double ax, double ay, double bx, double by) {
|
||||
// Here px=lat, py=lng; ax=lat, ay=lng, etc.
|
||||
final double x0 = px;
|
||||
final double y0 = py;
|
||||
final double x1 = ax;
|
||||
final double y1 = ay;
|
||||
final double x2 = bx;
|
||||
final double y2 = by;
|
||||
|
||||
final double dx = x2 - x1;
|
||||
final double dy = y2 - y1;
|
||||
double t = 0.0;
|
||||
final double len2 = dx * dx + dy * dy;
|
||||
if (len2 > 0) {
|
||||
t = ((x0 - x1) * dx + (y0 - y1) * dy) / len2;
|
||||
if (t < 0) t = 0;
|
||||
if (t > 1) t = 1;
|
||||
}
|
||||
final double projX = x1 + t * dx;
|
||||
final double projY = y1 + t * dy;
|
||||
|
||||
final double distMeters = _haversineDistanceMeters(x0, y0, projX, projY);
|
||||
return {'lat': projX, 'lng': projY, 't': t, 'dist': distMeters};
|
||||
}
|
||||
|
||||
/// Haversine distance (meters)
|
||||
double _haversineDistanceMeters(
|
||||
double lat1, double lng1, double lat2, double lng2) {
|
||||
final double R = 6371000.0;
|
||||
final double dLat = _deg2rad(lat2 - lat1);
|
||||
final double dLon = _deg2rad(lng2 - lng1);
|
||||
final double a = sin(dLat / 2) * sin(dLat / 2) +
|
||||
cos(_deg2rad(lat1)) * cos(_deg2rad(lat2)) * sin(dLon / 2) * sin(dLon / 2);
|
||||
final double c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
|
||||
double _deg2rad(double deg) => deg * pi / 180.0;
|
||||
357
siro_driver/lib/controller/home/payment/captain_wallet_controller.dart
Executable file
357
siro_driver/lib/controller/home/payment/captain_wallet_controller.dart
Executable file
@@ -0,0 +1,357 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/firebase/firbase_messge.dart';
|
||||
import 'package:siro_driver/controller/firebase/local_notification.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
|
||||
import '../../../views/widgets/mydialoug.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
|
||||
class CaptainWalletController extends GetxController {
|
||||
bool isLoading = false;
|
||||
final formKeyTransfer = GlobalKey<FormState>();
|
||||
final formKeyAccount = GlobalKey<FormState>();
|
||||
Map walletDate = {};
|
||||
Map walletDateVisa = {};
|
||||
Map walletDriverPointsDate = {};
|
||||
final formKey = GlobalKey<FormState>();
|
||||
String totalAmount = '0';
|
||||
double kazan = 0;
|
||||
String totalAmountVisa = '0';
|
||||
String totalPoints = '0';
|
||||
final amountFromBudgetController = TextEditingController();
|
||||
final newDriverPhoneController = TextEditingController();
|
||||
final phoneWallet = TextEditingController();
|
||||
final cardBank = TextEditingController();
|
||||
final bankCode = TextEditingController();
|
||||
|
||||
payFromBudget() async {
|
||||
if (formKey.currentState!.validate()) {
|
||||
var pointFromBudget = int.parse((amountFromBudgetController.text));
|
||||
|
||||
// await getPaymentId('fromBudgetToPoints',
|
||||
// int.parse((amountFromBudgetController.text)) * -1);
|
||||
var paymentToken3 =
|
||||
await generateToken((pointFromBudget * -1).toString());
|
||||
var paymentID = await getPaymentId(
|
||||
'fromBudgetToPoints', (pointFromBudget * -1).toString());
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'amount': (pointFromBudget * -1).toString(),
|
||||
'rideId': paymentID.toString(),
|
||||
'payment_method': 'myBudget',
|
||||
'passengerID': 'myBudgetToPoint',
|
||||
'token': paymentToken3,
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
Future.delayed(const Duration(seconds: 1));
|
||||
await addDriverWallet(
|
||||
'fromBudget', pointFromBudget.toString(), pointFromBudget.toString());
|
||||
update();
|
||||
Get.back();
|
||||
await refreshCaptainWallet();
|
||||
NotificationController().showNotification(
|
||||
'You have successfully charged your account'.tr,
|
||||
'$pointFromBudget ${'has been added to your budget'.tr}',
|
||||
'tone1',
|
||||
'',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future refreshCaptainWallet() async {
|
||||
await getCaptainWalletFromRide();
|
||||
await getCaptainWalletFromBuyPoints();
|
||||
// await checkAccountCaptainBank();
|
||||
}
|
||||
|
||||
List amountToNewDriverMap = [];
|
||||
bool isNewTransfer = false;
|
||||
Future detectNewDriverFromMyBudget() async {
|
||||
if (formKeyTransfer.currentState!.validate()) {
|
||||
if (int.parse(amountFromBudgetController.text) <
|
||||
double.parse(totalAmountVisa) &&
|
||||
int.parse(amountFromBudgetController.text) > 10) {
|
||||
//get new driver details
|
||||
isNewTransfer = true;
|
||||
update();
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getDriverDetails,
|
||||
payload: {'driver_phone': '+2${newDriverPhoneController.text}'});
|
||||
isNewTransfer = false;
|
||||
update();
|
||||
if (res != 'failure') {
|
||||
var d = jsonDecode(res);
|
||||
amountToNewDriverMap = d['data'];
|
||||
// update();
|
||||
} else {
|
||||
mySnackeBarError("This driver is not registered".tr);
|
||||
}
|
||||
} else {
|
||||
mySnackeBarError('Your Budget less than needed'.tr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future getCaptainWalletFromRide() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
var res = await CRUD().getWallet(
|
||||
link: AppLink.getAllPaymentFromRide,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
// isLoading = false;
|
||||
if (res != 'failure') {
|
||||
walletDate = jsonDecode(res);
|
||||
totalAmount = walletDate['message'][0]['total_amount'] ?? '0';
|
||||
update();
|
||||
var res1 = await CRUD().getWallet(
|
||||
link: AppLink.getAllPaymentVisa,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()});
|
||||
walletDateVisa = jsonDecode(res1);
|
||||
totalAmountVisa = walletDateVisa['message'][0]['diff'].toString();
|
||||
|
||||
update();
|
||||
} else {
|
||||
totalAmount = "0";
|
||||
totalAmountVisa = "0";
|
||||
}
|
||||
}
|
||||
|
||||
Future getCaptainWalletFromBuyPoints() async {
|
||||
// isLoading = true;
|
||||
update();
|
||||
|
||||
var res = await CRUD().getWallet(
|
||||
link: AppLink.getDriverPaymentPoints,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
isLoading = false;
|
||||
// update();
|
||||
|
||||
if (res != 'failure') {
|
||||
walletDriverPointsDate = jsonDecode(res);
|
||||
double totalPointsDouble = double.parse(
|
||||
walletDriverPointsDate['message'][0]['total_amount'].toString());
|
||||
totalPoints = totalPointsDouble.toStringAsFixed(0);
|
||||
} else {
|
||||
totalPoints = '0';
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
String paymentToken = '';
|
||||
Future<String> generateToken(String amount) async {
|
||||
var res =
|
||||
await CRUD().postWallet(link: AppLink.addPaymentTokenDriver, payload: {
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
'amount': amount.toString(),
|
||||
});
|
||||
var d = (res);
|
||||
return d['message'];
|
||||
}
|
||||
|
||||
// late String paymentID;
|
||||
Future<String> getPaymentId(String paymentMethod, amount) async {
|
||||
// paymentToken = await generateToken(amount);
|
||||
var res =
|
||||
await CRUD().postWallet(link: AppLink.addDriverPaymentPoints, payload: {
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
'amount': amount.toString(),
|
||||
'payment_method': paymentMethod.toString(),
|
||||
});
|
||||
var d = (res);
|
||||
// paymentID = d['message'].toString();
|
||||
return d['message'];
|
||||
}
|
||||
|
||||
Future addDriverWallet(String paymentMethod, point, count) async {
|
||||
paymentToken = await generateToken(count);
|
||||
var paymentID = await getPaymentId(paymentMethod, point.toString());
|
||||
await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: {
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
'paymentID': paymentID.toString(),
|
||||
'amount': point,
|
||||
'token': paymentToken,
|
||||
'paymentMethod': paymentMethod.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
Future addDriverPayment(String paymentMethod, point, wayPay) async {
|
||||
paymentToken = await generateToken(point);
|
||||
var paymentID = await getPaymentId(paymentMethod, point.toString());
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'amount': point.toString(),
|
||||
'rideId': paymentID.toString(),
|
||||
'payment_method': paymentMethod,
|
||||
'passengerID': wayPay,
|
||||
'token': paymentToken,
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
Future addDriverWalletFromPromo(String paymentMethod, point) async {
|
||||
var resPromotion =
|
||||
await CRUD().postWallet(link: AppLink.addpromotionDriver, payload: {
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
'payment_amount': point,
|
||||
'timePromo': paymentMethod,
|
||||
});
|
||||
if (resPromotion != 'failure') {
|
||||
paymentToken = await generateToken(point);
|
||||
var paymentID = await getPaymentId(paymentMethod, point.toString());
|
||||
var res =
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'amount': point,
|
||||
'rideId': paymentID.toString(),
|
||||
'payment_method': paymentMethod.toString(),
|
||||
'passengerID': paymentMethod,
|
||||
'token': paymentToken,
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
if (res != 'failure') {
|
||||
String title = 'wallet_updated'.tr; // Notification title
|
||||
String message = '${'wallet_credited_message'.tr} $point';
|
||||
String tone = 'default_tone'.tr; // Notification tone or sound
|
||||
String payLoad =
|
||||
'wallet_updated'; // Additional data payload for the notification
|
||||
|
||||
Get.find<NotificationController>()
|
||||
.showNotification(title, message, tone, payLoad);
|
||||
}
|
||||
} else {
|
||||
Get.back();
|
||||
mySnackeBarError(
|
||||
"A promotion record for this driver already exists for today.".tr);
|
||||
}
|
||||
}
|
||||
|
||||
Future addDriverWalletToInvitor(String paymentMethod, driverID, point) async {
|
||||
paymentToken = await generateToken(point);
|
||||
var paymentID = await getPaymentId(paymentMethod, point.toString());
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'driverID': driverID,
|
||||
'amount': point,
|
||||
'token': paymentToken,
|
||||
'rideId': paymentID.toString(),
|
||||
'payment_method': paymentMethod.toString(),
|
||||
'passengerID': paymentMethod,
|
||||
});
|
||||
await addSeferWallet(paymentMethod,
|
||||
(double.parse(point) * -2).toString()); // deduct 2 from sefer wallet
|
||||
}
|
||||
|
||||
Future addSeferWallet(String paymentMethod, String point) async {
|
||||
var seferToken = await generateToken(point.toString());
|
||||
await CRUD().postWallet(link: AppLink.addSeferWallet, payload: {
|
||||
'amount': point.toString(),
|
||||
'paymentMethod': paymentMethod,
|
||||
'passengerId': 'driver',
|
||||
'token': seferToken,
|
||||
'driverId': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
Future addTransferDriversWallet(String paymentMethod1, paymentMethod2) async {
|
||||
var paymentID =
|
||||
await getPaymentId(paymentMethod1, amountFromBudgetController.text);
|
||||
paymentToken = await generateToken(
|
||||
(int.parse(amountFromBudgetController.text) * -1).toString());
|
||||
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'amount': (int.parse(amountFromBudgetController.text) * -1).toString(),
|
||||
'rideId': paymentID.toString(),
|
||||
'payment_method': paymentMethod1,
|
||||
'passengerID': 'To ${amountToNewDriverMap[0]['id']}',
|
||||
'token': paymentToken,
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
|
||||
paymentID = await getPaymentId(paymentMethod2,
|
||||
(int.parse(amountFromBudgetController.text) - 5).toString());
|
||||
paymentToken = await generateToken(amountFromBudgetController.text);
|
||||
var res1 =
|
||||
await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: {
|
||||
'driverID': amountToNewDriverMap[0]['id'].toString(),
|
||||
'paymentID': paymentID.toString(),
|
||||
'amount': ((int.parse(amountFromBudgetController.text) - 5))
|
||||
// kazan) // double.parse(kazan) .08 for egypt
|
||||
.toStringAsFixed(
|
||||
0), // this will convert buddget to poitns by kazan .08
|
||||
|
||||
'token': paymentToken,
|
||||
'paymentMethod': paymentMethod2.toString(),
|
||||
});
|
||||
if (res1 != 'failure') {
|
||||
// Get.find<FirebaseMessagesController>().sendNotificationToDriverMAP(
|
||||
// 'Transfer',
|
||||
// '${'You have transfer to your wallet from'.tr}'
|
||||
// '${box.read(BoxName.nameDriver)}',
|
||||
// amountToNewDriverMap[0]['token'].toString(),
|
||||
// [],
|
||||
// 'order1.wav');
|
||||
NotificationService.sendNotification(
|
||||
target: amountToNewDriverMap[0]['token'].toString(),
|
||||
title: 'Transfer'.tr,
|
||||
body: '${'You have transfer to your wallet from'.tr}'
|
||||
'${box.read(BoxName.nameDriver)}',
|
||||
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'ding',
|
||||
driverList: [], category: 'Transfer',
|
||||
);
|
||||
await addSeferWallet('payout fee', '5');
|
||||
|
||||
Get.defaultDialog(
|
||||
title: 'transfer Successful'.tr,
|
||||
middleText: '',
|
||||
titleStyle: AppStyle.title,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () async {
|
||||
Get.back();
|
||||
Get.back();
|
||||
|
||||
await refreshCaptainWallet();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getKazanPercent() async {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getKazanPercent,
|
||||
payload: {'country': box.read(BoxName.countryCode).toString()},
|
||||
);
|
||||
if (res != 'failure') {
|
||||
var json = jsonDecode(res);
|
||||
kazan = double.parse(json['message'][0]['kazan'].toString());
|
||||
// naturePrice = double.parse(json['message'][0]['naturePrice']);
|
||||
// heavyPrice = double.parse(json['message'][0]['heavyPrice']);
|
||||
// latePrice = double.parse(json['message'][0]['latePrice']);
|
||||
// comfortPrice = double.parse(json['message'][0]['comfortPrice']);
|
||||
// speedPrice = double.parse(json['message'][0]['speedPrice']);
|
||||
// deliveryPrice = double.parse(json['message'][0]['deliveryPrice']);
|
||||
// mashwariPrice = double.parse(json['message'][0]['freePrice']);
|
||||
// fuelPrice = double.parse(json['message'][0]['fuelPrice']);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
// getKazanPercent();
|
||||
|
||||
await refreshCaptainWallet();
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
75
siro_driver/lib/controller/home/payment/credit_card_Controller.dart
Executable file
75
siro_driver/lib/controller/home/payment/credit_card_Controller.dart
Executable file
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../functions/digit_obsecur_formate.dart';
|
||||
import '../../functions/secure_storage.dart';
|
||||
|
||||
class CreditCardController extends GetxController {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
final TextEditingController cardNumberController = TextEditingController();
|
||||
final TextEditingController cardHolderNameController =
|
||||
TextEditingController();
|
||||
final TextEditingController expiryDateController = TextEditingController();
|
||||
final TextEditingController cvvCodeController = TextEditingController();
|
||||
openPayment() async {
|
||||
String? cardNumber = await SecureStorage().readData(BoxName.cardNumber);
|
||||
String? cardHolderName =
|
||||
await SecureStorage().readData(BoxName.cardHolderName);
|
||||
String? expiryDate = await SecureStorage().readData(BoxName.expiryDate);
|
||||
String? cvvCode = await SecureStorage().readData(BoxName.cvvCode);
|
||||
|
||||
if (cvvCode != null && cvvCode.isNotEmpty) {
|
||||
final maskedCardNumber = DigitObscuringFormatter()
|
||||
.formatEditUpdate(
|
||||
TextEditingValue.empty,
|
||||
TextEditingValue(text: cardNumber ?? ''),
|
||||
)
|
||||
.text;
|
||||
|
||||
cardNumberController.text = maskedCardNumber;
|
||||
cardHolderNameController.text = cardHolderName ?? '';
|
||||
expiryDateController.text = expiryDate ?? '';
|
||||
cvvCodeController.text = cvvCode;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
openPayment();
|
||||
// String? cardNumber = await SecureStorage().readData(BoxName.cardNumber);
|
||||
// String? cardHolderName =
|
||||
// await SecureStorage().readData(BoxName.cardHolderName);
|
||||
// String? expiryDate = await SecureStorage().readData(BoxName.expiryDate);
|
||||
// String? cvvCode = await SecureStorage().readData(BoxName.cvvCode);
|
||||
|
||||
// if (cvvCode != null && cvvCode.isNotEmpty) {
|
||||
// final maskedCardNumber = DigitObscuringFormatter()
|
||||
// .formatEditUpdate(
|
||||
// TextEditingValue.empty,
|
||||
// TextEditingValue(text: cardNumber ?? ''),
|
||||
// )
|
||||
// .text;
|
||||
|
||||
// cardNumberController.text = maskedCardNumber;
|
||||
// cardHolderNameController.text = cardHolderName ?? '';
|
||||
// expiryDateController.text = expiryDate ?? '';
|
||||
// cvvCodeController.text = cvvCode;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
class CreditCardModel {
|
||||
String cardNumber;
|
||||
String cardHolderName;
|
||||
String expiryDate;
|
||||
String cvvCode;
|
||||
|
||||
CreditCardModel({
|
||||
required this.cardNumber,
|
||||
required this.cardHolderName,
|
||||
required this.expiryDate,
|
||||
required this.cvvCode,
|
||||
});
|
||||
}
|
||||
235
siro_driver/lib/controller/home/payment/paymob_payout.dart
Executable file
235
siro_driver/lib/controller/home/payment/paymob_payout.dart
Executable file
@@ -0,0 +1,235 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/controller/payment/smsPaymnet/payment_services.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../views/widgets/mydialoug.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import 'captain_wallet_controller.dart';
|
||||
|
||||
class PaymobPayout extends GetxController {
|
||||
bool isLoading = false;
|
||||
String dropdownValue = 'etisalat';
|
||||
|
||||
int payOutFee = 5;
|
||||
payToDriverWallet(String amount, String issuer, String msisdn) async {
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAvailable) {
|
||||
// Authenticate the user
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment',
|
||||
// options: AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
// var dec = await CRUD()
|
||||
// .postWallet(link: AppLink.paymobPayoutDriverWallet, payload: {
|
||||
// "issuer": issuer,
|
||||
// "method": "wallet",
|
||||
// "amount": amount, //9.0,
|
||||
// "full_name":
|
||||
// '${box.read(BoxName.nameDriver)} ${box.read(BoxName.lastNameDriver)}',
|
||||
// "msisdn": msisdn, //"01010101010",
|
||||
// "bank_transaction_type": "cash_transfer"
|
||||
// });
|
||||
// if (dec['disbursement_status'] == 'successful') {
|
||||
// var paymentToken = await Get.find<CaptainWalletController>()
|
||||
// .generateToken(
|
||||
// ((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
|
||||
// .toStringAsFixed(0));
|
||||
// await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
// 'rideId': DateTime.now().toIso8601String(),
|
||||
// 'amount':
|
||||
// ((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
|
||||
// .toStringAsFixed(0),
|
||||
// 'payment_method': 'payout',
|
||||
// 'passengerID': 'myself',
|
||||
// 'token': paymentToken,
|
||||
// 'driverID': box.read(BoxName.driverID).toString(),
|
||||
// });
|
||||
// await Get.find<CaptainWalletController>()
|
||||
// .addSeferWallet('payout fee myself', payOutFee.toString());
|
||||
// await updatePaymentToPaid(box.read(BoxName.driverID).toString());
|
||||
// await sendEmail(
|
||||
// box.read(BoxName.driverID).toString(),
|
||||
// amount,
|
||||
// box.read(BoxName.phoneDriver).toString(),
|
||||
// box.read(BoxName.nameDriver).toString(),
|
||||
// 'Wallet',
|
||||
// box.read(BoxName.emailDriver).toString());
|
||||
|
||||
// mySnackbarSuccess('${'Transaction successful'.tr} ${dec['amount']}');
|
||||
|
||||
// Get.find<CaptainWalletController>().refreshCaptainWallet();
|
||||
// } else if (dec['disbursement_status'] == 'failed') {
|
||||
// mySnackeBarError('Transaction failed'.tr);
|
||||
// }
|
||||
} else {
|
||||
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog('Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future updatePaymentToPaid(String driverID) async {
|
||||
await CRUD().postWallet(link: AppLink.updatePaymetToPaid, payload: {
|
||||
'driverID': driverID.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
Future sendEmail(
|
||||
String driverId, amount, phone, name, bankCardNumber, email) async {
|
||||
await CRUD().sendEmail(AppLink.sendEmailToDrivertransaction, {
|
||||
"driverID": driverId,
|
||||
"total_amount": amount,
|
||||
"phone": phone,
|
||||
"name_arabic": name,
|
||||
"accountBank": bankCardNumber,
|
||||
"email": email
|
||||
});
|
||||
}
|
||||
|
||||
getAIKey(String key) async {
|
||||
var res =
|
||||
await CRUD().get(link: AppLink.getapiKey, payload: {"keyName": key});
|
||||
if (res != 'failure') {
|
||||
var d = jsonDecode(res)['message'];
|
||||
return d[key].toString();
|
||||
} else {}
|
||||
}
|
||||
|
||||
payToDriverBankAccount(
|
||||
String amount, String bankCardNumber, String bankCode) async {
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAvailable) {
|
||||
// Authenticate the user
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment',
|
||||
// options: AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
var body = {
|
||||
"issuer": "bank_card",
|
||||
"method": "bank_card",
|
||||
"amount": amount, //9.0,
|
||||
"full_name":
|
||||
'${box.read(BoxName.nameDriver)} ${box.read(BoxName.lastNameDriver)}',
|
||||
"bank_card_number": bankCardNumber, //"1111-2222-3333-4444",
|
||||
"bank_code": bankCode, //"CIB",
|
||||
"bank_transaction_type": "cash_transfer"
|
||||
};
|
||||
var dec = await CRUD().postWallet(
|
||||
link:
|
||||
'wl.tripz-egypt.com/v1/main/ride/payMob/paymob_driver/paymob_payout.php',
|
||||
payload: body,
|
||||
);
|
||||
if (dec['disbursement_status'] == 'successful') {
|
||||
var paymentToken = await Get.find<CaptainWalletController>()
|
||||
.generateToken(
|
||||
((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
|
||||
.toStringAsFixed(0));
|
||||
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||
'rideId': DateTime.now().toIso8601String(),
|
||||
'amount':
|
||||
((-1) * (double.parse(dec['amount'].toString())) - payOutFee)
|
||||
.toStringAsFixed(0),
|
||||
'payment_method': 'payout',
|
||||
'passengerID': 'myself',
|
||||
'token': paymentToken,
|
||||
'driverID': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
await Get.find<CaptainWalletController>()
|
||||
.addSeferWallet('payout fee myself', payOutFee.toString());
|
||||
await updatePaymentToPaid(box.read(BoxName.driverID).toString());
|
||||
await sendEmail(
|
||||
box.read(BoxName.driverID).toString(),
|
||||
amount,
|
||||
box.read(BoxName.phoneDriver).toString(),
|
||||
box.read(BoxName.nameDriver).toString(),
|
||||
'Wallet',
|
||||
box.read(BoxName.emailDriver).toString());
|
||||
|
||||
mySnackbarSuccess('${'Transaction successful'.tr} ${dec['amount']}');
|
||||
Get.find<CaptainWalletController>().refreshCaptainWallet();
|
||||
} else if (dec['disbursement_status'] == 'failed') {
|
||||
mySnackeBarError('Transaction failed'.tr);
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog('Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future payToWalletDriverAll(
|
||||
String amount, String issuer, String msisdn) async {
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAvailable) {
|
||||
// Authenticate the user
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment',
|
||||
// options: AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
await payToDriverWallet(amount, issuer, msisdn);
|
||||
} else {
|
||||
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog('Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future payToBankDriverAll(
|
||||
String amount, String bankCardNumber, String bankCode) async {
|
||||
bool isAvailable = await LocalAuthentication().isDeviceSupported();
|
||||
if (isAvailable) {
|
||||
// Authenticate the user
|
||||
bool didAuthenticate = await LocalAuthentication().authenticate(
|
||||
localizedReason: 'Use Touch ID or Face ID to confirm payment',
|
||||
// options: AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
sensitiveTransaction: true,
|
||||
);
|
||||
if (didAuthenticate) {
|
||||
await payToDriverBankAccount(amount, bankCardNumber, bankCode);
|
||||
} else {
|
||||
MyDialog().getDialog('Authentication failed'.tr, ''.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
MyDialog().getDialog('Biometric Authentication'.tr,
|
||||
'You should use Touch ID or Face ID to confirm payment'.tr, () {
|
||||
Get.back();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
147
siro_driver/lib/controller/home/points_for_rider_controller.dart
Executable file
147
siro_driver/lib/controller/home/points_for_rider_controller.dart
Executable file
@@ -0,0 +1,147 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
|
||||
import '../../constant/api_key.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../functions/crud.dart';
|
||||
import '../functions/location_controller.dart';
|
||||
|
||||
class PointsForRiderController extends GetxController {
|
||||
List<String> locations = [];
|
||||
String hintTextDestinationPoint = 'Search for your destination'.tr;
|
||||
TextEditingController placeStartController = TextEditingController();
|
||||
|
||||
void addLocation(String location) {
|
||||
locations.add(location);
|
||||
update();
|
||||
}
|
||||
|
||||
void getTextFromList(String location) {
|
||||
locations.add(location);
|
||||
update();
|
||||
Get.back();
|
||||
}
|
||||
|
||||
void removeLocation(int index) {
|
||||
locations.removeAt(index);
|
||||
update();
|
||||
}
|
||||
|
||||
void onReorder(int oldIndex, int newIndex) {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
update();
|
||||
}
|
||||
|
||||
final item = locations.removeAt(oldIndex);
|
||||
locations.insert(newIndex, item);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
class LocationModel {
|
||||
String name;
|
||||
double lat, lon;
|
||||
|
||||
LocationModel({required this.name, required this.lat, required this.lon});
|
||||
}
|
||||
|
||||
class WayPointController extends GetxController {
|
||||
// A list of text editing controllers for each text field
|
||||
// final textFields = [TextEditingController()].obs;
|
||||
List<String> wayPoints = [];
|
||||
List<List<dynamic>> placeListResponse = [];
|
||||
double wayPointHeight = 400;
|
||||
String hintTextDestinationPoint = 'Search for your destination'.tr;
|
||||
TextEditingController textSearchCotroller = TextEditingController();
|
||||
// A list of places corresponding to each text field
|
||||
final places = <String>[];
|
||||
|
||||
final hintTextPointList = <String>[];
|
||||
late LatLng myLocation;
|
||||
|
||||
void addWayPoints() {
|
||||
String wayPoint = 'Add a Stop'.tr;
|
||||
|
||||
if (wayPoints.length < 5) {
|
||||
wayPoints.add(wayPoint);
|
||||
update();
|
||||
} else {
|
||||
Get.defaultDialog(
|
||||
title: 'This is most WayPoints',
|
||||
titleStyle: AppStyle.title,
|
||||
middleText: '');
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void removeTextField(int index) {
|
||||
wayPoints.removeAt(index);
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
// A method to reorder the text fields and the places
|
||||
void reorderTextFields(int oldIndex, int newIndex) {
|
||||
if (newIndex > oldIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final wayPoint = wayPoints.removeAt(oldIndex);
|
||||
wayPoints.insert(newIndex, wayPoint);
|
||||
update();
|
||||
}
|
||||
|
||||
void updatePlace(int index, String input) async {
|
||||
var url =
|
||||
'${AppLink.googleMapsLink}place/nearbysearch/json?keyword=$input&location=${myLocation.latitude},${myLocation.longitude}&radius=50000&language=en&key=${AK.mapAPIKEY.toString()}';
|
||||
var response = await CRUD().getGoogleApi(link: url, payload: {});
|
||||
// final place = input;
|
||||
// if (index == 0) {
|
||||
List<dynamic> newList = [];
|
||||
placeListResponse.add(newList);
|
||||
newList = response['results'];
|
||||
placeListResponse[index].add(newList);
|
||||
update();
|
||||
// }
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
Get.put(LocationController());
|
||||
addWayPoints();
|
||||
myLocation = Get.find<LocationController>().myLocation;
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceList extends StatelessWidget {
|
||||
// Get the controller instance
|
||||
final controller = Get.put(WayPointController());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Use the Obx widget to rebuild the widget when the controller changes
|
||||
return Obx(() {
|
||||
// Use the ListView widget to display the list of places
|
||||
return ListView(
|
||||
// The children of the list are the places
|
||||
children: [
|
||||
// Loop through the places in the controller
|
||||
for (final place in controller.places)
|
||||
// Create a text widget for each place
|
||||
Text(
|
||||
// Use the place as the text
|
||||
place,
|
||||
|
||||
// Add some style and padding
|
||||
style: const TextStyle(fontSize: 18.0),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:siro_driver/controller/functions/encrypt_decrypt.dart';
|
||||
import 'package:siro_driver/env/env.dart';
|
||||
import 'package:siro_driver/print.dart';
|
||||
import 'package:siro_driver/views/widgets/error_snakbar.dart';
|
||||
import 'package:siro_driver/views/widgets/mydialoug.dart';
|
||||
|
||||
class ComplaintController extends GetxController {
|
||||
bool isLoading = false;
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final complaintController = TextEditingController();
|
||||
|
||||
List<dynamic> ridesList = [];
|
||||
Map<String, dynamic>? selectedRide;
|
||||
|
||||
Map<String, dynamic>? passengerReport;
|
||||
Map<String, dynamic>? driverReport;
|
||||
|
||||
var isUploading = false.obs;
|
||||
var uploadSuccess = false.obs;
|
||||
String audioLink = ''; // سيتم تخزين رابط الصوت هنا بعد الرفع
|
||||
String attachedFileName = '';
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
getLatestRidesForDriver();
|
||||
}
|
||||
|
||||
void _showCustomSnackbar(String title, String message,
|
||||
{bool isError = false}) {
|
||||
if (title.toLowerCase() == 'success') {
|
||||
mySnackbarSuccess(message.tr);
|
||||
} else if (isError) {
|
||||
mySnackeBarError(message.tr);
|
||||
} else {
|
||||
mySnackbarWarning(message.tr);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getLatestRidesForDriver() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
try {
|
||||
var res = await CRUD().get(link: AppLink.getRides, payload: {
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
});
|
||||
if (res != 'failure' && res != 'no_internet') {
|
||||
var decoded = jsonDecode(res);
|
||||
if (decoded['status'] == 'success') {
|
||||
ridesList = decoded['data'] ?? [];
|
||||
if (ridesList.isNotEmpty) {
|
||||
selectedRide = ridesList[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Error getting driver rides: $e");
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void selectRide(Map<String, dynamic> ride) {
|
||||
selectedRide = ride;
|
||||
audioLink = '';
|
||||
attachedFileName = '';
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> uploadAudioFile(File audioFile) async {
|
||||
try {
|
||||
isUploading.value = true;
|
||||
update();
|
||||
|
||||
var uri = Uri.parse(AppLink.uploadAudio);
|
||||
var request = http.MultipartRequest('POST', uri);
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
final String fingerPrint = box.read(BoxName.deviceFingerprint)?.toString() ?? '';
|
||||
|
||||
var mimeType = lookupMimeType(audioFile.path);
|
||||
request.headers.addAll({
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-FP': fingerPrint,
|
||||
});
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath(
|
||||
'audio',
|
||||
audioFile.path,
|
||||
contentType: mimeType != null ? MediaType.parse(mimeType) : null,
|
||||
),
|
||||
);
|
||||
|
||||
var response = await request.send();
|
||||
var responseBody = await http.Response.fromStream(response);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
var jsonResponse = jsonDecode(responseBody.body);
|
||||
if (jsonResponse['status'] == 'Audio file uploaded successfully.') {
|
||||
uploadSuccess.value = true;
|
||||
audioLink = jsonResponse['link'];
|
||||
attachedFileName = audioFile.path.split('/').last;
|
||||
_showCustomSnackbar('Success', 'Audio uploaded successfully.');
|
||||
} else {
|
||||
uploadSuccess.value = false;
|
||||
_showCustomSnackbar('Error', 'Failed to upload audio file.',
|
||||
isError: true);
|
||||
}
|
||||
} else {
|
||||
uploadSuccess.value = false;
|
||||
_showCustomSnackbar('Error', 'Server error: ${response.statusCode}',
|
||||
isError: true);
|
||||
}
|
||||
} catch (e) {
|
||||
uploadSuccess.value = false;
|
||||
_showCustomSnackbar(
|
||||
'Error', 'An application error occurred during upload.',
|
||||
isError: true);
|
||||
} finally {
|
||||
isUploading.value = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> submitComplaintToServer() async {
|
||||
if (!formKey.currentState!.validate() || complaintController.text.isEmpty) {
|
||||
_showCustomSnackbar(
|
||||
'Error', 'Please describe your issue before submitting.',
|
||||
isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedRide == null) {
|
||||
_showCustomSnackbar('Error', 'Please select a ride before submitting.',
|
||||
isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
final rideId = selectedRide!['id'].toString();
|
||||
final complaint = complaintController.text;
|
||||
|
||||
final responseData = await CRUD().post(
|
||||
link: AppLink.add_solve_all,
|
||||
payload: {
|
||||
'ride_id': rideId,
|
||||
'complaint_text': complaint,
|
||||
'audio_link': audioLink,
|
||||
},
|
||||
);
|
||||
|
||||
if (responseData == 'failure' || responseData == 'no_internet' || responseData == 'token_expired') {
|
||||
_showCustomSnackbar(
|
||||
'Error', 'Failed to connect to the server. Please try again.',
|
||||
isError: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (responseData['status'] == 'success') {
|
||||
passengerReport = responseData['data']['passenger_response'];
|
||||
driverReport = responseData['data']['driver_response'];
|
||||
update();
|
||||
|
||||
MyDialogContent().getDialog(
|
||||
'Success'.tr, Text('Your complaint has been submitted.'.tr), () {
|
||||
Get.back();
|
||||
complaintController.clear();
|
||||
audioLink = '';
|
||||
attachedFileName = '';
|
||||
formKey.currentState?.reset();
|
||||
});
|
||||
} else {
|
||||
String errorMessage =
|
||||
responseData['message'] ?? 'An unknown server error occurred'.tr;
|
||||
_showCustomSnackbar('Submission Failed', errorMessage, isError: true);
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Submit Complaint Error: $e");
|
||||
_showCustomSnackbar('Error', 'An application error occurred.'.tr,
|
||||
isError: true);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
44
siro_driver/lib/controller/home/profile/feed_back_controller.dart
Executable file
44
siro_driver/lib/controller/home/profile/feed_back_controller.dart
Executable file
@@ -0,0 +1,44 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/colors.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
|
||||
class FeedBackController extends GetxController {
|
||||
bool isLoading = false;
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final feedbackController = TextEditingController();
|
||||
|
||||
void addFeedBack() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
var res = await CRUD().post(link: AppLink.addFeedBack, payload: {
|
||||
'passengerId': box.read(BoxName.passengerID).toString(),
|
||||
'feedBack': feedbackController.text
|
||||
});
|
||||
var d = jsonDecode(res);
|
||||
if (d['status'].toString() == 'success') {
|
||||
Get.defaultDialog(
|
||||
title: 'Success'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
middleText: 'Feedback data saved successfully'.tr,
|
||||
middleTextStyle: AppStyle.title,
|
||||
confirm: MyElevatedButton(
|
||||
kolor: AppColor.greenColor,
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
35
siro_driver/lib/controller/home/profile/order_history_controller.dart
Executable file
35
siro_driver/lib/controller/home/profile/order_history_controller.dart
Executable file
@@ -0,0 +1,35 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/main.dart';
|
||||
|
||||
class OrderHistoryController extends GetxController {
|
||||
List<dynamic> orderHistoryListPassenger = [];
|
||||
bool isloading = true;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
getOrderHistoryByPassenger();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
Future getOrderHistoryByPassenger() async {
|
||||
var res = await CRUD().get(link: AppLink.getRides, payload: {
|
||||
'passenger_id': box.read(BoxName.passengerID).toString(),
|
||||
});
|
||||
if (res.toString() == 'failure') {
|
||||
// Get.snackbar('failure', 'message');
|
||||
isloading = false;
|
||||
update();
|
||||
} else {
|
||||
var jsonDecoded = jsonDecode(res);
|
||||
|
||||
orderHistoryListPassenger = jsonDecoded['data'];
|
||||
isloading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
42
siro_driver/lib/controller/home/profile/promos_controller.dart
Executable file
42
siro_driver/lib/controller/home/profile/promos_controller.dart
Executable file
@@ -0,0 +1,42 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/constant/style.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/views/widgets/elevated_btn.dart';
|
||||
|
||||
class PromosController extends GetxController {
|
||||
List<dynamic> promoList = [];
|
||||
bool isLoading = true;
|
||||
late String promos;
|
||||
@override
|
||||
void onInit() {
|
||||
getPromoByToday();
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
Future getPromoByToday() async {
|
||||
var res = await CRUD().get(link: AppLink.getPromoBytody, payload: {});
|
||||
if (res.toString() == 'failure') {
|
||||
Get.defaultDialog(
|
||||
title: 'No Promo for today .'.tr,
|
||||
middleText: '',
|
||||
titleStyle: AppStyle.title,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Back'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
Get.back();
|
||||
}));
|
||||
isLoading = false;
|
||||
update();
|
||||
} else {
|
||||
var jsonDecoded = jsonDecode(res);
|
||||
|
||||
promoList = jsonDecoded['message'];
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
164
siro_driver/lib/controller/home/splash_screen_controlle.dart
Executable file
164
siro_driver/lib/controller/home/splash_screen_controlle.dart
Executable file
@@ -0,0 +1,164 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
|
||||
import 'package:siro_driver/views/auth/captin/login_captin.dart';
|
||||
import 'package:siro_driver/views/home/on_boarding_page.dart';
|
||||
import '../functions/app_update_controller.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../main.dart';
|
||||
import '../../onbording_page.dart';
|
||||
import '../../print.dart';
|
||||
import '../functions/encrypt_decrypt.dart';
|
||||
import '../functions/package_info.dart';
|
||||
import '../functions/secure_storage.dart';
|
||||
import '../functions/security_checks.dart';
|
||||
|
||||
// Assuming you have a home page to navigate to after successful login.
|
||||
// If not, you might need to adjust the navigation target.
|
||||
// import 'package:siro_driver/views/home/home_page.dart';
|
||||
|
||||
class SplashScreenController extends GetxController
|
||||
with GetTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> animation;
|
||||
final progress = 0.0.obs;
|
||||
Timer? _progressTimer;
|
||||
|
||||
String packageInfo = '';
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
Get.put(AppUpdateController()); // تهيئة متحكم التحديثات الذكي
|
||||
_setupAnimations();
|
||||
_initializeAndNavigate();
|
||||
checkSecurity();
|
||||
}
|
||||
|
||||
checkSecurity() async {
|
||||
final random = Random();
|
||||
|
||||
if (random.nextBool()) {
|
||||
await SecurityHelper.performSecurityChecks();
|
||||
} else {
|
||||
await SecurityChecks.isDeviceRootedFromNative(Get.context!);
|
||||
}
|
||||
}
|
||||
|
||||
void _setupAnimations() {
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
);
|
||||
animation =
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut);
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
/// This is the core function that initializes the app.
|
||||
/// It runs two tasks simultaneously and navigates only when necessary.
|
||||
Future<void> _initializeAndNavigate() async {
|
||||
// Start getting package info, no need to wait for it.
|
||||
_getPackageInfo();
|
||||
|
||||
const minSplashDurationMs = 4000;
|
||||
_animateProgressBar(minSplashDurationMs);
|
||||
|
||||
// Define the two concurrent tasks
|
||||
final minDuration =
|
||||
Future.delayed(const Duration(milliseconds: minSplashDurationMs));
|
||||
final navigationTargetFuture = _getNavigationTarget();
|
||||
|
||||
// Wait for both tasks to complete
|
||||
await Future.wait([minDuration, navigationTargetFuture]);
|
||||
|
||||
// The future now returns a nullable Widget (Widget?)
|
||||
final Widget? targetPage = await navigationTargetFuture;
|
||||
|
||||
// *** FIX: Only navigate if the targetPage is not null. ***
|
||||
// This prevents navigating again if the login function already handled it.
|
||||
if (targetPage != null) {
|
||||
Get.off(() => targetPage,
|
||||
transition: Transition.fadeIn,
|
||||
duration: const Duration(milliseconds: 500));
|
||||
} else {
|
||||
Log.print(
|
||||
"Navigation was handled internally by the login process. Splash screen will not navigate.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Animates the progress bar over a given duration.
|
||||
void _animateProgressBar(int totalMilliseconds) {
|
||||
const interval = 50;
|
||||
int elapsed = 0;
|
||||
_progressTimer?.cancel();
|
||||
_progressTimer =
|
||||
Timer.periodic(const Duration(milliseconds: interval), (timer) {
|
||||
elapsed += interval;
|
||||
progress.value = (elapsed / totalMilliseconds).clamp(0.0, 1.0);
|
||||
if (elapsed >= totalMilliseconds) {
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Determines the correct page to navigate to, or returns null if navigation
|
||||
/// is expected to be handled by an internal process (like login).
|
||||
Future<Widget?> _getNavigationTarget() async {
|
||||
try {
|
||||
// 1) Onboarding
|
||||
final doneOnboarding = box.read(BoxName.onBoarding) == 'yes';
|
||||
if (!doneOnboarding) {
|
||||
// الأفضل: رجّع الواجهة بدل Get.off داخل الدالة
|
||||
return OnBoardingPage();
|
||||
}
|
||||
|
||||
// 2) Login
|
||||
final isDriverDataAvailable = box.read(BoxName.phoneDriver) != null;
|
||||
if (!isDriverDataAvailable) {
|
||||
return LoginCaptin();
|
||||
}
|
||||
|
||||
final loginController = Get.put(LoginDriverController());
|
||||
|
||||
final AppInitializer initializer = AppInitializer();
|
||||
await initializer.initializeApp();
|
||||
await EncryptionHelper.initialize();
|
||||
|
||||
await loginController.loginWithGoogleCredential(
|
||||
box.read(BoxName.driverID).toString(),
|
||||
box.read(BoxName.emailDriver).toString(),
|
||||
);
|
||||
|
||||
return null; // لأن loginWithGoogleCredential يوجّه
|
||||
} catch (e) {
|
||||
Log.print("Error during navigation logic: $e");
|
||||
return LoginCaptin();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _getPackageInfo() async {
|
||||
try {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
packageInfo = info.version;
|
||||
await box.write(BoxName.packagInfo, packageInfo);
|
||||
update(); // To update any UI element that might be listening
|
||||
} catch (e) {
|
||||
Log.print("Could not get package info: $e");
|
||||
packageInfo = '1.0.0'; // Default value
|
||||
await box.write(BoxName.packagInfo, packageInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_progressTimer?.cancel();
|
||||
_animationController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_driver/constant/box_name.dart';
|
||||
import 'package:siro_driver/constant/links.dart';
|
||||
import 'package:siro_driver/controller/functions/crud.dart';
|
||||
import 'package:siro_driver/models/model/driver/rides_summary_model.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
class StatisticsController extends GetxController {
|
||||
bool isLoading = false;
|
||||
|
||||
// ═══ Weekly Data ═══
|
||||
List<DayStat> weeklyStats = [];
|
||||
double weeklyEarnings = 0;
|
||||
int weeklyTrips = 0;
|
||||
double weeklyHours = 0;
|
||||
|
||||
// ═══ Monthly Data ═══
|
||||
List<MonthlyPriceDriverModel> monthlyEarnings = [];
|
||||
List<MonthlyRideModel> monthlyRides = [];
|
||||
List<MonthlyDataModel> monthlyDuration = [];
|
||||
double monthlyTotalEarnings = 0;
|
||||
int monthlyTotalTrips = 0;
|
||||
double monthlyTotalHours = 0;
|
||||
String bestDay = '--';
|
||||
double bestDayEarnings = 0;
|
||||
|
||||
// ═══ Tab State ═══
|
||||
int selectedTab = 0; // 0=weekly, 1=monthly
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
reloadData();
|
||||
}
|
||||
|
||||
void changeTab(int tab) {
|
||||
selectedTab = tab;
|
||||
update();
|
||||
}
|
||||
|
||||
// ═══════ جلب البيانات الأسبوعية ═══════
|
||||
Future<void> fetchWeeklyData() async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getWeeklyAggregate,
|
||||
payload: {
|
||||
'driver_id': box.read(BoxName.driverID).toString(),
|
||||
},
|
||||
);
|
||||
|
||||
if (res != null && res != 'failure') {
|
||||
var data = jsonDecode(res);
|
||||
if (data['message'] is List) {
|
||||
weeklyStats = (data['message'] as List)
|
||||
.map((e) => DayStat.fromJson(e))
|
||||
.toList();
|
||||
weeklyEarnings = weeklyStats.fold(0, (s, d) => s + d.earnings);
|
||||
weeklyTrips = weeklyStats.fold(0, (s, d) => s + d.trips);
|
||||
weeklyHours = weeklyStats.fold(0, (s, d) => s + d.hours);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Stats] Weekly fetch error: $e');
|
||||
// Fallback: generate from local data
|
||||
_generateLocalWeeklyData();
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
|
||||
// ═══════ جلب البيانات الشهرية ═══════
|
||||
Future<void> fetchMonthlyData() async {
|
||||
try {
|
||||
// 1. أرباح شهرية
|
||||
var earningsRes = await CRUD().getWallet(
|
||||
link: AppLink.getAllPaymentFromRide,
|
||||
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (earningsRes != null && earningsRes != 'failure') {
|
||||
var data = jsonDecode(earningsRes);
|
||||
if (data['message'] is List) {
|
||||
monthlyEarnings = (data['message'] as List)
|
||||
.map((e) => MonthlyPriceDriverModel.fromJson(e))
|
||||
.toList();
|
||||
monthlyTotalEarnings =
|
||||
monthlyEarnings.fold(0, (s, d) => s + d.pricePerDay);
|
||||
|
||||
// أفضل يوم
|
||||
if (monthlyEarnings.isNotEmpty) {
|
||||
var best = monthlyEarnings
|
||||
.reduce((a, b) => a.pricePerDay > b.pricePerDay ? a : b);
|
||||
bestDay = best.day.toString();
|
||||
bestDayEarnings = best.pricePerDay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. رحلات شهرية
|
||||
var ridesRes = await CRUD().get(
|
||||
link: AppLink.getRidesDriverByDay,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (ridesRes != null && ridesRes != 'failure') {
|
||||
var data = jsonDecode(ridesRes);
|
||||
if (data['message'] is List) {
|
||||
monthlyRides = (data['message'] as List)
|
||||
.map((e) => MonthlyRideModel.fromJson(e))
|
||||
.toList();
|
||||
monthlyTotalTrips = monthlyRides.fold(0, (s, d) => s + d.countRide);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. ساعات شهرية
|
||||
var durationRes = await CRUD().get(
|
||||
link: AppLink.getTotalDriverDuration,
|
||||
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||
);
|
||||
if (durationRes != null && durationRes != 'failure') {
|
||||
var data = jsonDecode(durationRes);
|
||||
if (data['message'] is List) {
|
||||
monthlyDuration = (data['message'] as List)
|
||||
.map((e) => MonthlyDataModel.fromJson(e))
|
||||
.toList();
|
||||
monthlyTotalHours =
|
||||
monthlyDuration.fold(0, (s, d) => s + d.totalDuration.toDouble());
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Stats] Monthly fetch error: $e');
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void _generateLocalWeeklyData() {
|
||||
// Fallback بيانات محلية عند عدم توفر الـ API
|
||||
final now = DateTime.now();
|
||||
weeklyStats = List.generate(7, (i) {
|
||||
final day = now.subtract(Duration(days: 6 - i));
|
||||
return DayStat(
|
||||
date: day,
|
||||
dayName: _getDayName(day.weekday),
|
||||
earnings: 0,
|
||||
trips: 0,
|
||||
hours: 0,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
String _getDayName(int weekday) {
|
||||
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
return days[weekday - 1];
|
||||
}
|
||||
|
||||
bool _isFetching = false;
|
||||
|
||||
Future<void> reloadData() async {
|
||||
if (_isFetching) return;
|
||||
_isFetching = true;
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
try {
|
||||
debugPrint('📊 [Statistics] Reloading data...');
|
||||
await fetchWeeklyData();
|
||||
await fetchMonthlyData();
|
||||
debugPrint('📊 [Statistics] Data reload complete.');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [Statistics] Error reloading data: $e');
|
||||
} finally {
|
||||
_isFetching = false;
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════ نموذج إحصائية اليوم ═══════
|
||||
class DayStat {
|
||||
final DateTime date;
|
||||
final String dayName;
|
||||
final double earnings;
|
||||
final int trips;
|
||||
final double hours;
|
||||
|
||||
DayStat({
|
||||
required this.date,
|
||||
required this.dayName,
|
||||
required this.earnings,
|
||||
required this.trips,
|
||||
required this.hours,
|
||||
});
|
||||
|
||||
factory DayStat.fromJson(Map<String, dynamic> json) {
|
||||
final date =
|
||||
DateTime.tryParse(json['day']?.toString() ?? '') ?? DateTime.now();
|
||||
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
return DayStat(
|
||||
date: date,
|
||||
dayName: dayNames[date.weekday - 1],
|
||||
earnings: double.tryParse(json['earnings']?.toString() ?? '0') ?? 0,
|
||||
trips: int.tryParse(json['trips']?.toString() ?? '0') ?? 0,
|
||||
hours: double.tryParse(json['hours']?.toString() ?? '0') ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
60
siro_driver/lib/controller/local/local_controller.dart
Executable file
60
siro_driver/lib/controller/local/local_controller.dart
Executable file
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../main.dart';
|
||||
import '../themes/themes.dart';
|
||||
import '../profile/setting_controller.dart';
|
||||
|
||||
class LocaleController extends GetxController {
|
||||
Locale? language;
|
||||
String countryCode = '';
|
||||
|
||||
ThemeData get appTheme {
|
||||
String lang = box.read(BoxName.lang) ?? 'en';
|
||||
bool isDarkMode = box.read('isDarkMode') ?? false;
|
||||
return _getThemeFor(lang, isDarkMode);
|
||||
}
|
||||
|
||||
ThemeData get lightTheme {
|
||||
String lang = box.read(BoxName.lang) ?? 'en';
|
||||
return _getThemeFor(lang, false);
|
||||
}
|
||||
|
||||
ThemeData get darkTheme {
|
||||
String lang = box.read(BoxName.lang) ?? 'en';
|
||||
return _getThemeFor(lang, true);
|
||||
}
|
||||
|
||||
ThemeData _getThemeFor(String lang, bool isDarkMode) {
|
||||
if (lang.startsWith('ar')) {
|
||||
return isDarkMode ? darkThemeArabic : lightThemeArabic;
|
||||
} else {
|
||||
return isDarkMode ? darkThemeEnglish : lightThemeEnglish;
|
||||
}
|
||||
}
|
||||
|
||||
void refreshTheme() {
|
||||
update();
|
||||
}
|
||||
|
||||
void changeLang(String langcode) {
|
||||
Locale locale = Locale(langcode);
|
||||
box.write(BoxName.lang, langcode);
|
||||
Get.updateLocale(locale);
|
||||
update();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
String? storedLang = box.read(BoxName.lang);
|
||||
if (storedLang == null) {
|
||||
storedLang = Get.deviceLocale!.languageCode;
|
||||
box.write(BoxName.lang, storedLang);
|
||||
}
|
||||
language = Locale(storedLang);
|
||||
super.onInit();
|
||||
}
|
||||
}
|
||||
|
||||
7574
siro_driver/lib/controller/local/phone_intel/countries.dart
Normal file
7574
siro_driver/lib/controller/local/phone_intel/countries.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,168 @@
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:siro_driver/controller/local/phone_intel/helpers.dart';
|
||||
|
||||
import 'countries.dart';
|
||||
|
||||
class PickerDialogStyle {
|
||||
final Color? backgroundColor;
|
||||
|
||||
final TextStyle? countryCodeStyle;
|
||||
|
||||
final TextStyle? countryNameStyle;
|
||||
|
||||
final Widget? listTileDivider;
|
||||
|
||||
final EdgeInsets? listTilePadding;
|
||||
|
||||
final EdgeInsets? padding;
|
||||
|
||||
final Color? searchFieldCursorColor;
|
||||
|
||||
final InputDecoration? searchFieldInputDecoration;
|
||||
|
||||
final EdgeInsets? searchFieldPadding;
|
||||
|
||||
final double? width;
|
||||
|
||||
PickerDialogStyle({
|
||||
this.backgroundColor,
|
||||
this.countryCodeStyle,
|
||||
this.countryNameStyle,
|
||||
this.listTileDivider,
|
||||
this.listTilePadding,
|
||||
this.padding,
|
||||
this.searchFieldCursorColor,
|
||||
this.searchFieldInputDecoration,
|
||||
this.searchFieldPadding,
|
||||
this.width,
|
||||
});
|
||||
}
|
||||
|
||||
class CountryPickerDialog extends StatefulWidget {
|
||||
final List<Country> countryList;
|
||||
final Country selectedCountry;
|
||||
final ValueChanged<Country> onCountryChanged;
|
||||
final String searchText;
|
||||
final List<Country> filteredCountries;
|
||||
final PickerDialogStyle? style;
|
||||
final String languageCode;
|
||||
|
||||
const CountryPickerDialog({
|
||||
Key? key,
|
||||
required this.searchText,
|
||||
required this.languageCode,
|
||||
required this.countryList,
|
||||
required this.onCountryChanged,
|
||||
required this.selectedCountry,
|
||||
required this.filteredCountries,
|
||||
this.style,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CountryPickerDialog> createState() => _CountryPickerDialogState();
|
||||
}
|
||||
|
||||
class _CountryPickerDialogState extends State<CountryPickerDialog> {
|
||||
late List<Country> _filteredCountries;
|
||||
late Country _selectedCountry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_selectedCountry = widget.selectedCountry;
|
||||
_filteredCountries = widget.filteredCountries.toList()
|
||||
..sort(
|
||||
(a, b) => a
|
||||
.localizedName(widget.languageCode)
|
||||
.compareTo(b.localizedName(widget.languageCode)),
|
||||
);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mediaWidth = MediaQuery.of(context).size.width;
|
||||
final width = widget.style?.width ?? mediaWidth;
|
||||
const defaultHorizontalPadding = 40.0;
|
||||
const defaultVerticalPadding = 24.0;
|
||||
return Dialog(
|
||||
insetPadding: EdgeInsets.symmetric(
|
||||
vertical: defaultVerticalPadding,
|
||||
horizontal: mediaWidth > (width + defaultHorizontalPadding * 2)
|
||||
? (mediaWidth - width) / 2
|
||||
: defaultHorizontalPadding),
|
||||
backgroundColor: widget.style?.backgroundColor,
|
||||
child: Container(
|
||||
padding: widget.style?.padding ?? const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding:
|
||||
widget.style?.searchFieldPadding ?? const EdgeInsets.all(0),
|
||||
child: TextField(
|
||||
cursorColor: widget.style?.searchFieldCursorColor,
|
||||
decoration: widget.style?.searchFieldInputDecoration ??
|
||||
InputDecoration(
|
||||
suffixIcon: const Icon(Icons.search),
|
||||
labelText: widget.searchText,
|
||||
),
|
||||
onChanged: (value) {
|
||||
_filteredCountries = widget.countryList.stringSearch(value)
|
||||
..sort(
|
||||
(a, b) => a
|
||||
.localizedName(widget.languageCode)
|
||||
.compareTo(b.localizedName(widget.languageCode)),
|
||||
);
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: _filteredCountries.length,
|
||||
itemBuilder: (ctx, index) => Column(
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
leading: kIsWeb
|
||||
? Image.asset(
|
||||
'assets/flags/${_filteredCountries[index].code.toLowerCase()}.png',
|
||||
package: 'intl_phone_field',
|
||||
width: 32,
|
||||
)
|
||||
: Text(
|
||||
_filteredCountries[index].flag,
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
contentPadding: widget.style?.listTilePadding,
|
||||
title: Text(
|
||||
_filteredCountries[index]
|
||||
.localizedName(widget.languageCode),
|
||||
style: widget.style?.countryNameStyle ??
|
||||
const TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
trailing: Text(
|
||||
'+${_filteredCountries[index].dialCode}',
|
||||
style: widget.style?.countryCodeStyle ??
|
||||
const TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
onTap: () {
|
||||
_selectedCountry = _filteredCountries[index];
|
||||
widget.onCountryChanged(_selectedCountry);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
widget.style?.listTileDivider ??
|
||||
const Divider(thickness: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
31
siro_driver/lib/controller/local/phone_intel/helpers.dart
Normal file
31
siro_driver/lib/controller/local/phone_intel/helpers.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'countries.dart';
|
||||
|
||||
bool isNumeric(String s) =>
|
||||
s.isNotEmpty && int.tryParse(s.replaceAll("+", "")) != null;
|
||||
|
||||
String removeDiacritics(String str) {
|
||||
var withDia =
|
||||
'ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇçÐÌÍÎÏìíîïÙÚÛÜùúûüÑñŠšŸÿýŽž';
|
||||
var withoutDia =
|
||||
'AAAAAAaaaaaaOOOOOOOooooooEEEEeeeeeCcDIIIIiiiiUUUUuuuuNnSsYyyZz';
|
||||
|
||||
for (int i = 0; i < withDia.length; i++) {
|
||||
str = str.replaceAll(withDia[i], withoutDia[i]);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
extension CountryExtensions on List<Country> {
|
||||
List<Country> stringSearch(String search) {
|
||||
search = removeDiacritics(search.toLowerCase());
|
||||
return where(
|
||||
(country) => isNumeric(search) || search.startsWith("+")
|
||||
? country.dialCode.contains(search)
|
||||
: removeDiacritics(country.name.replaceAll("+", "").toLowerCase())
|
||||
.contains(search) ||
|
||||
country.nameTranslations.values.any((element) =>
|
||||
removeDiacritics(element.toLowerCase()).contains(search)),
|
||||
).toList();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user