Update: 2026-06-14 05:48:58

This commit is contained in:
Hamza-Ayed
2026-06-14 05:48:58 +03:00
parent 2645ed0cf1
commit 8e3b9eca4d
22 changed files with 789 additions and 179 deletions

View File

@@ -17,6 +17,7 @@ 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/country_logic.dart';
import '../../functions/sms_egypt_controller.dart';
class RegisterCaptainController extends GetxController {
@@ -385,11 +386,13 @@ class RegisterCaptainController extends GetxController {
if (formKey.currentState!.validate()) {
isLoading = true;
update();
final fixedPhone =
CountryLogic.formatCurrentCountryPhone(phoneController.text);
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,
'phone': fixedPhone,
'password': passwordController.text,
'gender': sex,
'site': address,

View File

@@ -211,9 +211,11 @@ class FirebaseMessagesController extends GetxController {
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'ding', '');
}
MyDialog().getDialog(title, body, () {
// Empty callback, MyDialog already closes itself using pop().
});
MyDialog().getChatDialog(
title.isNotEmpty ? title : 'message From passenger'.tr,
body,
() {},
);
break;
case 'token change':

View File

@@ -83,17 +83,55 @@ class CountryLogic {
/// Helper to format phone using the current country in box.
static String formatCurrentCountryPhone(String phone) {
String cleanPhone = phone.replaceAll(RegExp(r'[ \-\(\)]'), '').trim();
if (cleanPhone.startsWith('+963') || cleanPhone.startsWith('00963')) {
String cleanPhone = phone.replaceAll(RegExp(r'[ \-\(\)\+]'), '').trim();
// 1. Explicit International Code Detection
if (cleanPhone.startsWith('00963')) {
cleanPhone = cleanPhone.replaceFirst('00963', '963');
}
if (cleanPhone.startsWith('00962')) {
cleanPhone = cleanPhone.replaceFirst('00962', '962');
}
if (cleanPhone.startsWith('0020')) {
cleanPhone = cleanPhone.replaceFirst('0020', '20');
}
if (cleanPhone.startsWith('963')) {
return formatPhone(cleanPhone, 'Syria');
}
if (cleanPhone.startsWith('+20') || cleanPhone.startsWith('0020')) {
if (cleanPhone.startsWith('962')) {
return formatPhone(cleanPhone, 'Jordan');
}
if (cleanPhone.startsWith('20')) {
return formatPhone(cleanPhone, 'Egypt');
}
if (cleanPhone.startsWith('+962') || cleanPhone.startsWith('00962')) {
// 2. Local/National Format Detection by Country-Specific Mobile Prefixes
// Jordan: 07x / 7x (9 national digits)
if (cleanPhone.startsWith('07') && cleanPhone.length == 10) {
return formatPhone(cleanPhone, 'Jordan');
}
if (cleanPhone.startsWith('7') && cleanPhone.length == 9) {
return formatPhone(cleanPhone, 'Jordan');
}
// Syria: 09x / 9x (9 national digits)
if (cleanPhone.startsWith('09') && cleanPhone.length == 10) {
return formatPhone(cleanPhone, 'Syria');
}
if (cleanPhone.startsWith('9') && cleanPhone.length == 9) {
return formatPhone(cleanPhone, 'Syria');
}
// Egypt: 01x (10 national digits) / 1x (9 national digits)
if (cleanPhone.startsWith('01') && cleanPhone.length == 11) {
return formatPhone(cleanPhone, 'Egypt');
}
if (cleanPhone.startsWith('1') && cleanPhone.length == 10) {
return formatPhone(cleanPhone, 'Egypt');
}
// 3. Fallback: Default to current user's country code saved in box
final country = box.read(BoxName.countryCode) ?? 'Syria';
return formatPhone(cleanPhone, country);
}

View File

@@ -0,0 +1,30 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
class TranslateHelper {
static Future<String> translateText(String text, String targetLang) async {
if (text.isEmpty) return text;
try {
final url = Uri.parse(
'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=$targetLang&dt=t&q=${Uri.encodeComponent(text)}'
);
final response = await http.get(url);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
if (decoded != null && decoded is List && decoded.isNotEmpty && decoded[0] is List) {
final List parts = decoded[0];
String translated = '';
for (var part in parts) {
if (part is List && part.isNotEmpty) {
translated += part[0].toString();
}
}
return translated;
}
}
} catch (e) {
// Fallback to original text on any exception
}
return text;
}
}

View File

@@ -67,12 +67,16 @@ class OrderRequestController extends GetxController
String timeToPassenger = "Calculating...".tr;
String distanceToPassenger = "--";
String apiStartName = "";
String apiEndName = "";
// --- الخريطة ---
Set<Polyline> polylines = {};
bool _hasCalculatedFullJourney = false;
// حالة التطبيق والصوت
bool isInBackground = false;
bool isAccepting = false;
final AudioPlayer audioPlayer = AudioPlayer();
@override
@@ -288,6 +292,15 @@ class OrderRequestController extends GetxController
totalTripDuration = tripResult['duration_text'];
polylines.add(tripResult['polyline']);
if (tripResult['start_name'] != null &&
tripResult['start_name'].toString().isNotEmpty) {
apiStartName = tripResult['start_name'].toString();
}
if (tripResult['end_name'] != null &&
tripResult['end_name'].toString().isNotEmpty) {
apiEndName = tripResult['end_name'].toString();
}
// 🔥 تخزين استجابة السيرفر كاملة (بما فيها الـ points والـ instructions)
if (tripResult['raw_response'] != null) {
box.write('cached_trip_route', tripResult['raw_response']);
@@ -385,6 +398,8 @@ class OrderRequestController extends GetxController
'duration_text': durationText,
'polyline': polyline,
'encoded_polyline': encodedPoints,
'start_name': data['startName']?.toString(),
'end_name': data['endName']?.toString(),
'raw_response': response.body, // 🔥 نمرر الـ JSON كاملاً
};
}
@@ -618,91 +633,102 @@ class OrderRequestController extends GetxController
// Accept Order Logic
Future<void> acceptOrder() async {
if (isAccepting) return;
isAccepting = true;
update();
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();
try {
// 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),
});
} else {
// ✅ حالة النجاح
// حماية من الكراش: التأكد من وجود HomeCaptainController قبل استخدامه
if (!Get.isRegistered<HomeCaptainController>()) {
Get.put(HomeCaptainController());
} else {
Get.find<HomeCaptainController>().changeRideId();
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;
}
box.write(BoxName.statusDriverLocation, 'on');
changeApplied();
if (isFailure) {
// ⛔ حالة الفشل: الطلب مأخوذ
MyDialog().getDialog(
"Sorry, the order was taken by another driver.".tr, '', () {
// بما أن MyDialog يغلق نفسه الآن، نحتاج Get.back() واحدة فقط لإغلاق صفحة الطلب
Get.back();
});
} else {
// ✅ حالة النجاح
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),
};
// حماية من الكراش: التأكد من وجود HomeCaptainController قبل استخدامه
if (!Get.isRegistered<HomeCaptainController>()) {
Get.put(HomeCaptainController());
} else {
Get.find<HomeCaptainController>().changeRideId();
}
box.write(BoxName.rideArguments, rideArgs);
box.write(BoxName.statusDriverLocation, 'on');
changeApplied();
// الانتقال النهائي
Get.off(() => PassengerLocationMapPage(), arguments: rideArgs);
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);
}
} catch (e) {
Log.print("Error in acceptOrder: $e");
} finally {
isAccepting = false;
update();
}
}

View File

@@ -412,10 +412,21 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
}
}
// ==============================================================================
// 🟢 دوال معالجة قبول الطلب وتجهيز المتغيرات 🟢
// ==============================================================================
bool _isProcessingAccept = false;
// ==============================================================================
// 🟢 دوال معالجة قبول الطلب وتجهيز المتغيرات 🟢
// ==============================================================================
Future<void> _processAcceptOrder(List<dynamic> data) async {
if (_isProcessingAccept) {
print("⏳ _processAcceptOrder: Already accepting order, skipping duplicate request.");
return;
}
_isProcessingAccept = true;
Get.dialog(
WillPopScope(
onWillPop: () async => false,
@@ -485,6 +496,8 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
}
print("❌ Error in accept process: $e");
Get.snackbar("خطأ", "حدث خطأ غير متوقع");
} finally {
_isProcessingAccept = false;
}
}

View File

@@ -1,13 +1,12 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/style.dart';
import 'package:siro_driver/views/widgets/elevated_btn.dart';
import 'package:siro_driver/controller/home/captin/map_driver_controller.dart';
import 'package:siro_driver/constant/currency.dart';
import '../../../constant/colors.dart';
import '../../../controller/functions/location_controller.dart';
import '../../../main.dart';
import '../../Rate/rate_passenger.dart';
import '../../widgets/my_textField.dart';
import 'mapDriverWidgets/driver_end_ride_bar.dart';
@@ -15,7 +14,6 @@ import 'mapDriverWidgets/google_driver_map_page.dart';
import 'mapDriverWidgets/google_map_app.dart';
import 'mapDriverWidgets/passenger_info_window.dart';
import 'mapDriverWidgets/sos_connect.dart';
import 'mapDriverWidgets/sped_circle.dart';
class PassengerLocationMapPage extends StatelessWidget {
PassengerLocationMapPage({super.key});
@@ -409,7 +407,7 @@ class PricesWindow extends StatelessWidget {
),
const SizedBox(height: 10),
Text(
'${controller.totalCost} ${'\$'.tr}',
'${controller.totalCost} ${CurrencyHelper.currency}',
style: AppStyle.headTitle2.copyWith(
color: Theme.of(context).textTheme.bodyLarge?.color,
fontSize: 42,

View File

@@ -21,6 +21,7 @@ import '../../../../controller/home/navigation/navigation_view.dart';
import '../../../../controller/profile/setting_controller.dart';
import '../../../../env/env.dart';
import '../../../../main.dart';
import '../../../../print.dart';
import '../../../notification/available_rides_page.dart';
import '../driver_map_page.dart';
import 'widget/connect.dart';
@@ -603,11 +604,13 @@ class _FloatingControls extends StatelessWidget {
),
// Continue active ride
if (box.read(BoxName.rideStatus) == 'Applied' ||
box.read(BoxName.rideStatus) == 'Apply' ||
box.read(BoxName.rideStatus) == 'Begin') ...[
const SizedBox(height: 10),
_Fab(
onTap: () {
if (box.read(BoxName.rideStatus) == 'Applied') {
final status = box.read(BoxName.rideStatus);
if (status == 'Applied' || status == 'Apply') {
Get.to(() => PassengerLocationMapPage(),
arguments: box.read(BoxName.rideArguments));
Get.put(MapDriverController())
@@ -669,5 +672,27 @@ class _Fab extends StatelessWidget {
// Helper
// ─────────────────────────────────────────────
Future<void> checkForAppliedRide(BuildContext context) async {
checkForPendingOrderFromServer();
if (Get.currentRoute == '/passenger-location-map') return;
final localStatus = box.read(BoxName.rideStatus);
final localArgs = box.read(BoxName.rideArguments) ??
box.read(BoxName.rideArgumentsFromBackground);
if ((localStatus == 'Apply' ||
localStatus == 'Applied' ||
localStatus == 'Begin') &&
localArgs != null &&
localArgs != 'failure') {
Log.print("🔄 Auto-recovering active ride: status=$localStatus");
if (localStatus == 'Apply' || localStatus == 'Applied') {
Get.to(() => PassengerLocationMapPage(), arguments: localArgs);
Get.put(MapDriverController()).changeRideToBeginToPassenger();
} else if (localStatus == 'Begin') {
Get.to(() => PassengerLocationMapPage(), arguments: localArgs);
Get.put(MapDriverController()).startRideFromStartApp();
}
} else {
// If no local active ride, check the server
await checkForPendingOrderFromServer();
}
}

View File

@@ -219,14 +219,16 @@ class _Divider extends StatelessWidget {
// Server Helpers (unchanged logic)
// ─────────────────────────────────────────────
bool _isCheckingPendingOrder = false;
Future<void> checkForPendingOrderFromServer() async {
bool isProcessing = false;
if (isProcessing) return;
if (_isCheckingPendingOrder) return;
if (Get.currentRoute == '/passenger-location-map') return;
final driverId = box.read(BoxName.driverID)?.toString();
if (driverId == null) return;
isProcessing = true;
_isCheckingPendingOrder = true;
try {
var response = await CRUD().post(
@@ -258,7 +260,7 @@ Future<void> checkForPendingOrderFromServer() async {
} catch (_) {
// silent
} finally {
isProcessing = false;
_isCheckingPendingOrder = false;
}
}

View File

@@ -15,6 +15,7 @@ import '../../../../controller/firebase/notification_service.dart';
import '../../../../controller/functions/crud.dart';
import '../../../../constant/links.dart';
import '../../../widgets/my_textField.dart';
import '../../../../views/widgets/elevated_btn.dart';
class PassengerInfoWindow extends StatelessWidget {
const PassengerInfoWindow({super.key});
@@ -251,7 +252,7 @@ class PassengerInfoWindow extends StatelessWidget {
// --- الأزرار الرئيسية (وصلت / ابدأ) ---
if (!controller.isRideBegin)
_buildActionButtons(controller),
_buildActionButtons(context, controller),
// --- زر الإلغاء المدفوع (بعد انتهاء وقت الانتظار) ---
if (controller.isdriverWaitTimeEnd &&
@@ -372,7 +373,7 @@ class PassengerInfoWindow extends StatelessWidget {
);
}
Widget _buildActionButtons(MapDriverController controller) {
Widget _buildActionButtons(BuildContext context, MapDriverController controller) {
if (controller.isArrivedSend) {
return SizedBox(
width: double.infinity,
@@ -413,33 +414,33 @@ class PassengerInfoWindow extends StatelessWidget {
// وقد يُسبب Get.back() اللاحق إغلاق صفحة الماب بدلاً من الـ loading dialog
Get.defaultDialog(
title: "Start Trip?".tr,
titleStyle: const TextStyle(
titleStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColor.writeColor,
),
middleText: "Ensure the passenger is in the car.".tr,
middleTextStyle: TextStyle(
fontSize: 14,
color: AppColor.grayColor,
),
backgroundColor: Theme.of(context).cardColor,
barrierDismissible: true,
radius: 14,
confirm: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF27AE60),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
radius: 18,
confirm: MyElevatedButton(
title: 'Start'.tr,
onPressed: () async {
// نُغلق الديالوج بـ Get.back() لضمان أن GetX يعرف أنه أُغلق
Get.back();
// ثم نُنفذ startRideFromDriver الذي يستخدم Get.dialog و Get.back بأمان
await controller.startRideFromDriver();
},
child: Text('Start'.tr,
style: const TextStyle(fontWeight: FontWeight.bold)),
kolor: AppColor.greenColor,
),
cancel: TextButton(
onPressed: () => Get.back(),
child: Text('Cancel'.tr,
style: const TextStyle(color: Colors.grey)),
child: Text(
'Cancel'.tr,
style: TextStyle(color: AppColor.grayColor, fontWeight: FontWeight.w600),
),
),
);
},

View File

@@ -9,13 +9,12 @@ import 'package:siro_driver/constant/api_key.dart';
import 'package:siro_driver/models/overlay_service.dart';
import '../../../../constant/box_name.dart';
import '../../../../constant/links.dart';
import '../../../../controller/firebase/firbase_messge.dart';
import '../../../../controller/firebase/local_notification.dart';
import '../../../../controller/firebase/notification_service.dart';
import '../../../../controller/functions/crud.dart';
import '../../../../main.dart';
import '../../../../models/model/order_data.dart';
import '../../../../print.dart';
import '../../../../constant/currency.dart';
// === Enhanced Colors for Better Readability ===
class AppColors {
@@ -510,7 +509,7 @@ class _OrderOverlayState extends State<OrderOverlay>
flex: 3,
child: _buildHighlightInfo(
// FIX: Use the parsed priceValue here
"${NumberFormat('#,##0').format(priceValue)} ل.س",
"${NumberFormat('#,##0').format(priceValue)} ${CurrencyHelper.currency}",
"السعر".tr,
Icons.monetization_on_rounded,
AppColors.priceHighlight,

View File

@@ -4,6 +4,7 @@ import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:siro_driver/constant/api_key.dart';
import 'package:siro_driver/constant/colors.dart';
import 'package:siro_driver/controller/home/captin/order_request_controller.dart';
import 'package:siro_driver/constant/currency.dart';
class OrderRequestPage extends StatelessWidget {
const OrderRequestPage({super.key});
@@ -43,10 +44,12 @@ class OrderRequestPage extends StatelessWidget {
final String passengerName =
getValue(8).isEmpty ? "عميل" : getValue(8);
final String startAddr =
getValue(29).isEmpty ? "موقع الانطلاق" : getValue(29);
final String endAddr =
getValue(30).isEmpty ? "الوجهة" : getValue(30);
final String startAddr = controller.apiStartName.isNotEmpty
? controller.apiStartName
: (getValue(29).isEmpty ? "موقع الانطلاق" : getValue(29));
final String endAddr = controller.apiEndName.isNotEmpty
? controller.apiEndName
: (getValue(30).isEmpty ? "الوجهة" : getValue(30));
final bool isVisa = (getValue(13) == 'true');
// منطق Speed = سعر ثابت
@@ -194,7 +197,7 @@ class OrderRequestPage extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text("${controller.tripPrice} ل.س",
Text("${controller.tripPrice} ${CurrencyHelper.currency}",
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@@ -294,12 +297,12 @@ class OrderRequestPage extends StatelessWidget {
children: [
const Icon(Icons.my_location,
size: 18, color: Colors.green),
Expanded(
child: Container(
width: 2,
color: Colors.grey.shade300,
margin: const EdgeInsets.symmetric(
vertical: 2))),
Container(
width: 2,
height: 38,
color: Colors.grey.shade300,
margin: const EdgeInsets.symmetric(
vertical: 4)),
const Icon(Icons.location_on,
size: 18, color: Colors.red),
],
@@ -307,8 +310,6 @@ class OrderRequestPage extends StatelessWidget {
const SizedBox(width: 15),
Expanded(
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
@@ -319,15 +320,16 @@ class OrderRequestPage extends StatelessWidget {
style: TextStyle(
fontSize: 11,
color: Colors.grey)),
const SizedBox(height: 2),
Text(startAddr,
style: const TextStyle(
fontSize: 14,
fontSize: 13,
fontWeight: FontWeight.w600),
maxLines: 1,
maxLines: 2,
overflow: TextOverflow.ellipsis),
],
),
const Spacer(),
const SizedBox(height: 14),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
@@ -336,11 +338,12 @@ class OrderRequestPage extends StatelessWidget {
style: TextStyle(
fontSize: 11,
color: Colors.grey)),
const SizedBox(height: 2),
Text(endAddr,
style: const TextStyle(
fontSize: 14,
fontSize: 13,
fontWeight: FontWeight.w600),
maxLines: 1,
maxLines: 2,
overflow: TextOverflow.ellipsis),
],
),
@@ -373,7 +376,9 @@ class OrderRequestPage extends StatelessWidget {
const SizedBox(width: 15),
Expanded(
child: ElevatedButton(
onPressed: () => controller.acceptOrder(),
onPressed: controller.isAccepting
? null
: () => controller.acceptOrder(),
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
foregroundColor: Colors.white,
@@ -386,24 +391,35 @@ class OrderRequestPage extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("قبول الرحلة",
style: TextStyle(
Text(
controller.isAccepting
? "جاري القبول...".tr
: "قبول الرحلة".tr,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold)),
const SizedBox(width: 15),
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
value: controller.progress,
color: Colors.white,
strokeWidth: 2.5,
backgroundColor: Colors.white24),
child: controller.isAccepting
? const CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
)
: CircularProgressIndicator(
value: controller.progress,
color: Colors.white,
strokeWidth: 2.5,
backgroundColor: Colors.white24),
),
const SizedBox(width: 8),
Text("${controller.remainingTime}",
style: const TextStyle(
fontSize: 14, color: Colors.white)),
if (!controller.isAccepting) ...[
const SizedBox(width: 8),
Text("${controller.remainingTime}",
style: const TextStyle(
fontSize: 14,
color: Colors.white)),
],
],
),
),

View File

@@ -6,6 +6,7 @@ import 'package:get/get.dart';
import 'package:siro_driver/constant/colors.dart';
import 'package:siro_driver/constant/style.dart';
import 'package:siro_driver/controller/functions/tts.dart';
import 'package:siro_driver/controller/functions/translate_helper.dart';
class DialogConfig {
static const Duration animationDuration = Duration(milliseconds: 200);
@@ -136,6 +137,158 @@ class MyDialog extends GetxController {
barrierColor: Colors.black.withAlpha(102), // 0.4 opacity
);
}
void getChatDialog(String title, String body, VoidCallback onPressed) {
final textToSpeechController = Get.put(TextToSpeechController());
HapticFeedback.mediumImpact();
String currentText = body;
bool isTranslated = false;
bool isLoading = false;
Get.dialog(
StatefulBuilder(
builder: (context, setState) {
final theme = Theme.of(context);
return TweenAnimationBuilder<double>(
duration: DialogConfig.animationDuration,
tween: Tween(begin: 0.0, end: 1.0),
builder: (context, value, child) {
return Transform.scale(
scale: 0.95 + (0.05 * value),
child: Opacity(opacity: value, child: child),
);
},
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: DialogConfig.blurStrength,
sigmaY: DialogConfig.blurStrength,
),
child: Builder(builder: (context) {
return CupertinoAlertDialog(
title: Column(
children: [
Text(
title.tr,
style: AppStyle.title.copyWith(
fontSize: 20,
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
color: AppColor.primaryColor,
),
),
const SizedBox(height: 8),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
CupertinoButton(
padding: const EdgeInsets.all(8),
onPressed: () async {
HapticFeedback.selectionClick();
await textToSpeechController.speakText(title);
await textToSpeechController.speakText(currentText);
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color:
AppColor.primaryColor.withAlpha(26), // 0.1 opacity
borderRadius: BorderRadius.circular(8),
),
child: Icon(
CupertinoIcons.speaker_2_fill,
color: AppColor.primaryColor,
size: 24,
),
),
),
const SizedBox(height: 8),
if (isLoading)
const Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: CupertinoActivityIndicator(),
),
)
else
Text(
currentText,
style: AppStyle.title.copyWith(
fontSize: 16,
height: 1.3,
color: theme.textTheme.bodyLarge?.color ?? AppColor.writeColor,
),
textAlign: TextAlign.center,
),
],
),
actions: [
CupertinoDialogAction(
onPressed: () async {
if (isLoading) return;
HapticFeedback.lightImpact();
if (isTranslated) {
setState(() {
currentText = body;
isTranslated = false;
});
} else {
setState(() {
isLoading = true;
});
try {
final targetLang = Get.locale?.languageCode ?? 'ar';
final translated = await TranslateHelper.translateText(body, targetLang);
setState(() {
currentText = translated;
isTranslated = true;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
}
}
},
child: Text(
isTranslated ? 'Original'.tr : 'Translate'.tr,
style: TextStyle(
color: AppColor.blueColor,
fontWeight: FontWeight.w600,
fontSize: 17,
),
),
),
CupertinoDialogAction(
onPressed: () {
HapticFeedback.mediumImpact();
Navigator.of(context, rootNavigator: true).pop();
onPressed();
},
child: Text(
'OK'.tr,
style: TextStyle(
color: AppColor.greenColor,
fontWeight: FontWeight.w600,
fontSize: 17,
),
),
),
],
);
}),
),
);
},
),
barrierDismissible: true,
barrierColor: Colors.black.withAlpha(102), // 0.4 opacity
);
}
}
class MyDialogContent extends GetxController {

View File

@@ -117,10 +117,11 @@ class PhoneAuthHelper {
String? email,
}) async {
try {
final fixedPhone = CountryLogic.formatCurrentCountryPhone(phoneNumber);
final response = await CRUD().post(
link: _registerUrl,
payload: {
'phone_number': phoneNumber,
'phone_number': fixedPhone,
'first_name': firstName,
'last_name': lastName,
'email': email ?? '', // Send empty string if null

View File

@@ -19,6 +19,7 @@ import 'package:siro_rider/controller/voice_call_controller.dart';
import '../home/map/ride_lifecycle_controller.dart';
import '../home/map/ride_state.dart';
import 'local_notification.dart';
import '../../views/widgets/mydialoug.dart';
class FirebaseMessagesController extends GetxController {
final fcmToken = FirebaseMessaging.instance;
@@ -171,14 +172,22 @@ class FirebaseMessagesController extends GetxController {
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'ding');
}
passengerDialog(body);
MyDialog().getChatDialog(
title.isNotEmpty ? title : 'message From passenger'.tr,
body,
() {},
);
update();
} else if (category == 'message From Driver') {
// <-- كان 'message From Driver'
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'ding');
}
passengerDialog(body);
MyDialog().getChatDialog(
title.isNotEmpty ? title : 'message From Driver'.tr,
body,
() {},
);
update();
} else if (category == 'Trip is Begin') {
// <-- كان 'Trip is Begin'

View File

@@ -81,17 +81,55 @@ class CountryLogic {
/// Helper to format phone using the current country in box.
static String formatCurrentCountryPhone(String phone) {
String cleanPhone = phone.replaceAll(RegExp(r'[ \-\(\)]'), '').trim();
if (cleanPhone.startsWith('+963') || cleanPhone.startsWith('00963')) {
String cleanPhone = phone.replaceAll(RegExp(r'[ \-\(\)\+]'), '').trim();
// 1. Explicit International Code Detection
if (cleanPhone.startsWith('00963')) {
cleanPhone = cleanPhone.replaceFirst('00963', '963');
}
if (cleanPhone.startsWith('00962')) {
cleanPhone = cleanPhone.replaceFirst('00962', '962');
}
if (cleanPhone.startsWith('0020')) {
cleanPhone = cleanPhone.replaceFirst('0020', '20');
}
if (cleanPhone.startsWith('963')) {
return formatPhone(cleanPhone, 'Syria');
}
if (cleanPhone.startsWith('+20') || cleanPhone.startsWith('0020')) {
if (cleanPhone.startsWith('962')) {
return formatPhone(cleanPhone, 'Jordan');
}
if (cleanPhone.startsWith('20')) {
return formatPhone(cleanPhone, 'Egypt');
}
if (cleanPhone.startsWith('+962') || cleanPhone.startsWith('00962')) {
// 2. Local/National Format Detection by Country-Specific Mobile Prefixes
// Jordan: 07x / 7x (9 national digits)
if (cleanPhone.startsWith('07') && cleanPhone.length == 10) {
return formatPhone(cleanPhone, 'Jordan');
}
if (cleanPhone.startsWith('7') && cleanPhone.length == 9) {
return formatPhone(cleanPhone, 'Jordan');
}
// Syria: 09x / 9x (9 national digits)
if (cleanPhone.startsWith('09') && cleanPhone.length == 10) {
return formatPhone(cleanPhone, 'Syria');
}
if (cleanPhone.startsWith('9') && cleanPhone.length == 9) {
return formatPhone(cleanPhone, 'Syria');
}
// Egypt: 01x (10 national digits) / 1x (9 national digits)
if (cleanPhone.startsWith('01') && cleanPhone.length == 11) {
return formatPhone(cleanPhone, 'Egypt');
}
if (cleanPhone.startsWith('1') && cleanPhone.length == 10) {
return formatPhone(cleanPhone, 'Egypt');
}
// 3. Fallback: Default to current user's country code saved in box
final country = box.read(BoxName.countryCode) ?? 'Jordan';
return formatPhone(cleanPhone, country);
}

View File

@@ -9,7 +9,7 @@ void showInBrowser(String url) async {
} else {}
}
Future<void> makePhoneCall(String phoneNumber) async {
String cleanAndFormatPhoneNumber(String phoneNumber) {
String formattedNumber = phoneNumber.replaceAll(RegExp(r'\s+'), '');
if (formattedNumber.length > 6) {
@@ -18,6 +18,11 @@ Future<void> makePhoneCall(String phoneNumber) async {
formattedNumber = '+$formattedNumber';
}
}
return formattedNumber;
}
Future<void> makePhoneCall(String phoneNumber) async {
String formattedNumber = cleanAndFormatPhoneNumber(phoneNumber);
// ملاحظة: الأرقام القصيرة (مثل 112) ستتجاوز الشرط أعلاه وتبقى "112" وهو الصحيح
// 3. التنفيذ (Launch)
@@ -44,23 +49,26 @@ Future<void> makePhoneCall(String phoneNumber) async {
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':
url = 'tel:$contactInfo';
url = 'tel:$formattedContact';
break;
case 'sms':
url = 'sms:$contactInfo?body=${Uri.encodeComponent(message)}';
url = 'sms:$formattedContact?body=${Uri.encodeComponent(message)}';
break;
case 'whatsapp':
url =
'https://api.whatsapp.com/send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
'https://api.whatsapp.com/send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
break;
case 'email':
url =
'mailto:$contactInfo?subject=Subject&body=${Uri.encodeComponent(message)}';
'mailto:$formattedContact?subject=Subject&body=${Uri.encodeComponent(message)}';
break;
default:
return;
@@ -68,11 +76,11 @@ void launchCommunication(
} else if (Platform.isAndroid) {
switch (method) {
case 'phone':
url = 'tel:$contactInfo';
url = 'tel:$formattedContact';
break;
case 'sms':
url = 'sms:$contactInfo?body=${Uri.encodeComponent(message)}';
url = 'sms:$formattedContact?body=${Uri.encodeComponent(message)}';
break;
case 'whatsapp':
// Check if WhatsApp is installed
@@ -80,16 +88,16 @@ void launchCommunication(
await canLaunchUrl(Uri.parse('whatsapp://'));
if (whatsappInstalled) {
url =
'whatsapp://send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
'whatsapp://send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
} else {
// Provide an alternative action, such as opening the WhatsApp Web API
url =
'https://api.whatsapp.com/send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
'https://api.whatsapp.com/send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
}
break;
case 'email':
url =
'mailto:$contactInfo?subject=Subject&body=${Uri.encodeComponent(message)}';
'mailto:$formattedContact?subject=Subject&body=${Uri.encodeComponent(message)}';
break;
default:
return;

View File

@@ -0,0 +1,30 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
class TranslateHelper {
static Future<String> translateText(String text, String targetLang) async {
if (text.isEmpty) return text;
try {
final url = Uri.parse(
'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=$targetLang&dt=t&q=${Uri.encodeComponent(text)}'
);
final response = await http.get(url);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
if (decoded != null && decoded is List && decoded.isNotEmpty && decoded[0] is List) {
final List parts = decoded[0];
String translated = '';
for (var part in parts) {
if (part is List && part.isNotEmpty) {
translated += part[0].toString();
}
}
return translated;
}
}
} catch (e) {
// Fallback to original text on any exception
}
return text;
}
}

View File

@@ -293,7 +293,7 @@ class ApplyOrderWidget extends StatelessWidget {
const Icon(Icons.star_rounded,
color: Colors.amber, size: 14),
Text(
" ${controller.driverRate}${controller.model}",
" ${controller.driverRate}${controller.model}${controller.carColor.isNotEmpty ? '${controller.carColor.tr}' : ''}",
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
@@ -356,23 +356,25 @@ class ApplyOrderWidget extends StatelessWidget {
final bool isBike = vehicleText.contains('scooter') ||
vehicleText.contains('bike') ||
vehicleText.contains('دراجة');
return Container(
height: 40, // تصغير من 50
width: 40,
decoration: BoxDecoration(
color: carColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(4),
child: ColorFiltered(
colorFilter: ColorFilter.mode(carColor, BlendMode.srcIn),
child: Image.asset(
isBike ||
box.read(BoxName.carType) == 'Scooter' ||
box.read(BoxName.carType) == 'Pink Bike'
? 'assets/images/moto.png'
: 'assets/images/car3.png',
fit: BoxFit.contain,
return AnimatedCarIcon(
child: Container(
height: 40, // تصغير من 50
width: 40,
decoration: BoxDecoration(
color: carColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(4),
child: ColorFiltered(
colorFilter: ColorFilter.mode(carColor, BlendMode.srcIn),
child: Image.asset(
isBike ||
box.read(BoxName.carType) == 'Scooter' ||
box.read(BoxName.carType) == 'Pink Bike'
? 'assets/images/moto.png'
: 'assets/images/car3.png',
fit: BoxFit.contain,
),
),
),
);
@@ -841,3 +843,48 @@ class TimeDriverToPassenger extends StatelessWidget {
});
}
}
class AnimatedCarIcon extends StatefulWidget {
final Widget child;
const AnimatedCarIcon({Key? key, required this.child}) : super(key: key);
@override
State<AnimatedCarIcon> createState() => _AnimatedCarIconState();
}
class _AnimatedCarIconState extends State<AnimatedCarIcon>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<Offset> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: true);
_animation = Tween<Offset>(
begin: const Offset(-0.06, 0.0),
end: const Offset(0.06, 0.0),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SlideTransition(
position: _animation,
child: widget.child,
);
}
}

View File

@@ -162,7 +162,7 @@ class RideBeginPassenger extends StatelessWidget {
children: [
Flexible(
child: Text(
'${controller.model}',
'${controller.model}${controller.carColor.isNotEmpty ? "${controller.carColor.tr}" : ""}',
style: TextStyle(fontSize: 12, color: AppColor.grayColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,

View File

@@ -97,6 +97,15 @@ class VipRideBeginPassenger extends StatelessWidget {
controller.model,
style: AppStyle.title,
),
if (controller.carColor.isNotEmpty) ...[
const SizedBox(
width: 10,
),
Text(
controller.carColor.tr,
style: AppStyle.title,
),
],
],
),
),

View File

@@ -7,6 +7,7 @@ import 'package:get/get.dart';
import '../../constant/colors.dart';
import '../../constant/style.dart';
import '../../controller/functions/tts.dart';
import '../../controller/functions/translate_helper.dart';
// ─────────────────────────────────────────────────────────────────────────────
// Config
@@ -417,6 +418,167 @@ class MyDialog extends GetxController {
barrierColor: _DC.barrierColor,
);
}
void getChatDialog(
String title,
String messageContent,
VoidCallback onPressed, {
IconData? icon,
}) {
HapticFeedback.mediumImpact();
String displayedText = messageContent;
bool isTranslated = false;
bool isLoading = false;
Get.dialog(
StatefulBuilder(
builder: (dialogContext, setState) {
return _DialogShell(
child: _GlassCard(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ── Body ──────────────────────────────────────────────
Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 20),
child: Column(
children: [
// Icon badge
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.primaryColor.withOpacity(0.1),
border: Border.all(
color: AppColor.primaryColor.withOpacity(0.2),
),
),
child: Icon(
icon ?? Icons.chat_bubble_outline_rounded,
color: AppColor.primaryColor,
size: 26,
),
),
const SizedBox(height: 16),
// Title
Text(
title.tr,
textAlign: TextAlign.center,
style: AppStyle.title.copyWith(
fontSize: 18,
fontWeight: FontWeight.w700,
letterSpacing: -0.4,
color: AppColor.writeColor,
),
),
const SizedBox(height: 10),
if (isLoading)
const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: CupertinoActivityIndicator(radius: 12),
),
)
else
Text(
displayedText,
textAlign: TextAlign.center,
style: AppStyle.subtitle.copyWith(
fontSize: 14.5,
height: 1.5,
color: Colors.grey[600],
),
),
const SizedBox(height: 16),
// TTS button
_SpeakButton(
texts: [title.tr, displayedText],
),
],
),
),
// ── Actions ───────────────────────────────────────────
Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.grey.withOpacity(0.15), width: 1),
),
),
child: Row(
children: [
// Translate Toggle
Expanded(
child: _ActionButton(
label: isTranslated ? 'Original'.tr : 'Translate'.tr,
color: AppColor.blueColor,
backgroundColor: AppColor.blueColor.withOpacity(0.07),
onPressed: () async {
if (isLoading) return;
HapticFeedback.lightImpact();
if (isTranslated) {
setState(() {
displayedText = messageContent;
isTranslated = false;
});
} else {
setState(() {
isLoading = true;
});
try {
final targetLang = Get.locale?.languageCode ?? 'ar';
final translated = await TranslateHelper.translateText(messageContent, targetLang);
setState(() {
displayedText = translated;
isTranslated = true;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
}
}
},
isLeft: true,
),
),
Container(width: 1, height: 52, color: Colors.grey.withOpacity(0.15)),
// Confirm
Expanded(
child: _ActionButton(
label: 'OK'.tr,
color: AppColor.primaryColor,
backgroundColor: AppColor.primaryColor.withOpacity(0.07),
onPressed: () {
HapticFeedback.mediumImpact();
Navigator.of(dialogContext, rootNavigator: true).pop();
Future.delayed(const Duration(milliseconds: 100), () {
onPressed();
});
},
isLeft: false,
isBold: true,
),
),
],
),
),
],
),
),
);
},
),
barrierDismissible: true,
barrierColor: _DC.barrierColor,
);
}
}
// ─────────────────────────────────────────────────────────────────────────────