Fixes & Updates - 2026-06-01: Integrate Back-End v3 updates, fix call/connection issues across apps
This commit is contained in:
@@ -5,6 +5,17 @@ import 'package:Intaleq/controller/firebase/local_notification.dart';
|
||||
import 'package:Intaleq/controller/home/deep_link_controller.dart';
|
||||
import 'package:Intaleq/controller/local/local_controller.dart';
|
||||
import 'package:Intaleq/controller/functions/tts.dart';
|
||||
import 'package:Intaleq/controller/voice_call_controller.dart';
|
||||
|
||||
import 'package:Intaleq/controller/home/map/map_socket_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/map_engine_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/location_search_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/nearby_drivers_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ui_interactions_controller.dart';
|
||||
import 'package:Intaleq/controller/home/menu_controller.dart';
|
||||
import 'package:Intaleq/controller/functions/crud.dart';
|
||||
import 'package:Intaleq/controller/home/points_for_rider_controller.dart';
|
||||
|
||||
/// This is the central dependency injection file for the app.
|
||||
/// It uses GetX Bindings to make the app start faster and manage memory better.
|
||||
@@ -39,5 +50,19 @@ class AppBindings extends Bindings {
|
||||
|
||||
// TextToSpeechController for global accessibility
|
||||
Get.lazyPut(() => TextToSpeechController(), fenix: true);
|
||||
|
||||
// VoiceCallController for WebRTC calls
|
||||
Get.lazyPut(() => VoiceCallController(), fenix: true);
|
||||
|
||||
// Map & Ride controllers registered globally to prevent route-disposal race conditions.
|
||||
Get.put(MapSocketController(), permanent: true);
|
||||
Get.put(MapEngineController(), permanent: true);
|
||||
Get.put(LocationSearchController(), permanent: true);
|
||||
Get.put(NearbyDriversController(), permanent: true);
|
||||
Get.put(RideLifecycleController(), permanent: true);
|
||||
Get.put(UiInteractionsController(), permanent: true);
|
||||
Get.put(MyMenuController(), permanent: true);
|
||||
Get.put(CRUD(), permanent: true);
|
||||
Get.put(WayPointController(), permanent: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +231,7 @@ class AppLink {
|
||||
static String addMishwari = "$server/ride/mishwari/add.php";
|
||||
static String cancelMishwari = "$server/ride/mishwari/cancel.php";
|
||||
static String getMishwari = "$server/ride/mishwari/get.php";
|
||||
static String sendChatMessage = "$server/ride/chat/send_message.php";
|
||||
|
||||
//-----------------DriverOrder------------------
|
||||
|
||||
|
||||
@@ -107,13 +107,14 @@ class PhoneAuthHelper {
|
||||
|
||||
/// Verifies the OTP and logs the user in.
|
||||
|
||||
static Future<void> verifyOtp(String phoneNumber) async {
|
||||
static Future<void> verifyOtp(String phoneNumber, String otpCode) async {
|
||||
try {
|
||||
final fixedPhone = formatSyrianPhone(phoneNumber);
|
||||
final response = await CRUD().post(
|
||||
link: _verifyOtpUrl,
|
||||
payload: {
|
||||
'phone_number': fixedPhone,
|
||||
'otp': otpCode,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -16,8 +16,9 @@ import '../../views/Rate/rate_captain.dart';
|
||||
import '../../views/home/map_page_passenger.dart';
|
||||
import '../../views/home/profile/promos_passenger_page.dart';
|
||||
import '../auth/google_sign.dart';
|
||||
import '../functions/audio_record1.dart';
|
||||
import '../home/map_passenger_controller.dart';
|
||||
import 'package:Intaleq/controller/voice_call_controller.dart';
|
||||
import '../home/map/ride_lifecycle_controller.dart';
|
||||
import '../home/map/ride_state.dart';
|
||||
import 'local_notification.dart';
|
||||
|
||||
class FirebaseMessagesController extends GetxController {
|
||||
@@ -105,8 +106,8 @@ class FirebaseMessagesController extends GetxController {
|
||||
// اقرأ "النوع" من حمولة البيانات، وليس من العنوان
|
||||
String category = message.data['category'] ?? '';
|
||||
|
||||
final mapCtrl = Get.isRegistered<MapPassengerController>()
|
||||
? Get.find<MapPassengerController>()
|
||||
final mapCtrl = Get.isRegistered<RideLifecycleController>()
|
||||
? Get.find<RideLifecycleController>()
|
||||
: null;
|
||||
// اقرأ العنوان (للعرض)
|
||||
String title = message.data['title'] ?? message.notification?.title ?? '';
|
||||
@@ -167,8 +168,8 @@ class FirebaseMessagesController extends GetxController {
|
||||
GoogleSignInHelper.signOut();
|
||||
} else if (category == 'Driver Is Going To Passenger') {
|
||||
// <-- كان 'Driver Is Going To Passenger'
|
||||
Get.find<MapPassengerController>().isDriverInPassengerWay = true;
|
||||
Get.find<MapPassengerController>().update();
|
||||
Get.find<RideLifecycleController>().isDriverInPassengerWay = true;
|
||||
Get.find<RideLifecycleController>().update();
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'tone1');
|
||||
}
|
||||
@@ -214,7 +215,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
}
|
||||
|
||||
if (driverList.isNotEmpty) {
|
||||
Get.find<MapPassengerController>()
|
||||
Get.find<RideLifecycleController>()
|
||||
.processRideFinished(driverList, source: "FCM");
|
||||
}
|
||||
} else if (category == 'Finish Monitor') {
|
||||
@@ -232,9 +233,9 @@ class FirebaseMessagesController extends GetxController {
|
||||
Log.print("🔔 FCM: Ride Cancelled by Driver received.");
|
||||
|
||||
// لا داعي لكتابة منطق التنظيف هنا، الكنترولر يتكفل بكل شيء
|
||||
if (Get.isRegistered<MapPassengerController>()) {
|
||||
if (Get.isRegistered<RideLifecycleController>()) {
|
||||
// استدعاء الحارس (سيتجاهل الأمر إذا كان السوكيت قد سبقه)
|
||||
Get.find<MapPassengerController>()
|
||||
Get.find<RideLifecycleController>()
|
||||
.processRideCancelledByDriver(message.data, source: "FCM");
|
||||
}
|
||||
|
||||
@@ -247,6 +248,19 @@ class FirebaseMessagesController extends GetxController {
|
||||
// ... (باقي الحالات مثل Call Income, Call End, إلخ) ...
|
||||
// ... بنفس الطريقة ...
|
||||
|
||||
else if (category == '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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
else if (category == 'Order Applied') {
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(
|
||||
@@ -277,7 +291,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
// var myList = jsonDecode(driverListJson) as List<dynamic>;
|
||||
// Log.print('myList: ${myList}');
|
||||
|
||||
// final controller = Get.find<MapPassengerController>();
|
||||
// final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
// // استدعاء الدالة الموحدة الجديدة التي أنشأناها
|
||||
// await controller.processRideAcceptance(
|
||||
@@ -311,8 +325,8 @@ class FirebaseMessagesController extends GetxController {
|
||||
// }
|
||||
// GoogleSignInHelper.signOut();
|
||||
// } else if (message.notification!.title! == 'Driver Is Going To Passenger') {
|
||||
// Get.find<MapPassengerController>().isDriverInPassengerWay = true;
|
||||
// Get.find<MapPassengerController>().update();
|
||||
// Get.find<RideLifecycleController>().isDriverInPassengerWay = true;
|
||||
// Get.find<RideLifecycleController>().update();
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification('Driver is Going To You'.tr,
|
||||
// 'Please stay on the picked point.'.tr, 'tone1');
|
||||
@@ -341,13 +355,13 @@ class FirebaseMessagesController extends GetxController {
|
||||
|
||||
// // (تم حذف الإشعار المحلي من هنا، نُقل إلى الدالة الموحدة)
|
||||
|
||||
// final controller = Get.find<MapPassengerController>();
|
||||
// final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
// // استدعاء حارس البوابة الجديد والآمن
|
||||
// controller.processRideBegin();
|
||||
|
||||
// // (تم حذف كل الأوامر التالية من هنا)
|
||||
// // Get.find<MapPassengerController>().getBeginRideFromDriver();
|
||||
// // Get.find<RideLifecycleController>().getBeginRideFromDriver();
|
||||
// // box.write(BoxName.passengerWalletTotal, '0');
|
||||
// // update();
|
||||
// } else if (message.notification!.title! == 'Hi ,I will go now'.tr) {
|
||||
@@ -360,7 +374,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
// update();
|
||||
// } // ... داخل معالج الإشعارات (FCM Handler) ...
|
||||
// if (message.notification!.title! == 'Hi ,I Arrive your site'.tr) {
|
||||
// final controller = Get.find<MapPassengerController>();
|
||||
// final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
// // 1. التأكد أننا في الحالة الصحيحة (السائق كان في الطريق)
|
||||
// if (controller.currentRideState.value == RideState.driverApplied) {
|
||||
@@ -383,7 +397,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
// title: 'Ok'.tr,
|
||||
// onPressed: () async {
|
||||
// Get.back();
|
||||
// await Get.find<MapPassengerController>()
|
||||
// await Get.find<RideLifecycleController>()
|
||||
// .reSearchAfterCanceledFromDriver();
|
||||
// },
|
||||
// ),
|
||||
@@ -394,7 +408,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
// Get.offAll(() => const MapPagePassenger());
|
||||
// },
|
||||
// )
|
||||
// // Get.find<MapPassengerController>()
|
||||
// // Get.find<RideLifecycleController>()
|
||||
// // .searchNewDriverAfterRejectingFromDriver();
|
||||
// );
|
||||
// } else if (message.notification!.title! == 'Driver Finish Trip'.tr) {
|
||||
@@ -434,7 +448,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
// box.write(BoxName.passengerWalletTotal, 0);
|
||||
// }
|
||||
|
||||
// Get.find<MapPassengerController>().tripFinishedFromDriver();
|
||||
// Get.find<RideLifecycleController>().tripFinishedFromDriver();
|
||||
|
||||
// NotificationController().showNotification(
|
||||
// 'Don’t forget your personal belongings.'.tr,
|
||||
@@ -535,7 +549,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
// box.write(BoxName.parentTripSelected, false);
|
||||
// box.remove(BoxName.tokenParent);
|
||||
|
||||
// Get.find<MapPassengerController>().restCounter();
|
||||
// Get.find<RideLifecycleController>().restCounter();
|
||||
// Get.offAll(() => const MapPagePassenger());
|
||||
// }
|
||||
// // else if (message.notification!.title! == 'Order Applied') {
|
||||
@@ -595,8 +609,8 @@ class FirebaseMessagesController extends GetxController {
|
||||
// Get.find<FirebaseMessagesController>().sendNotificationToPassengerToken(
|
||||
// 'Hi ,I will go now'.tr,
|
||||
// 'I will go now'.tr,
|
||||
// Get.find<MapPassengerController>().driverToken, []);
|
||||
// Get.find<MapPassengerController>()
|
||||
// Get.find<RideLifecycleController>().driverToken, []);
|
||||
// Get.find<RideLifecycleController>()
|
||||
// .startTimerDriverWaitPassenger5Minute();
|
||||
|
||||
Get.back();
|
||||
@@ -639,12 +653,12 @@ class DriverTipWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// Text(
|
||||
// '${'Your fee is '.tr}${Get.find<MapPassengerController>().totalPassenger.toStringAsFixed(2)}'),
|
||||
// '${'Your fee is '.tr}${Get.find<RideLifecycleController>().totalPassenger.toStringAsFixed(2)}'),
|
||||
Text(
|
||||
'Do you want to pay Tips for this Driver'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
|
||||
@@ -30,7 +30,7 @@ class AudioRecorderController extends GetxController {
|
||||
}
|
||||
|
||||
// Start recording
|
||||
Future<void> startRecording() async {
|
||||
Future<void> startRecording({String? rideId}) async {
|
||||
final bool isPermissionGranted = await recorder.hasPermission();
|
||||
if (!isPermissionGranted) {
|
||||
// RecordingPermissionException('l');
|
||||
@@ -38,10 +38,12 @@ class AudioRecorderController extends GetxController {
|
||||
}
|
||||
|
||||
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 using the current timestamp
|
||||
String fileName =
|
||||
// '${DateTime.now().year}-${DateTime.now().month}-${DateTime.now().day}_${Get.find<MapPassengerController>().rideId}.m4a';
|
||||
'${DateTime.now().year}-${DateTime.now().month}-${DateTime.now().day}.m4a';
|
||||
String fileName = (rideId != null && rideId.isNotEmpty && rideId != 'yet' && rideId != 'null')
|
||||
? '${dateStr}_$rideId.m4a'
|
||||
: '$dateStr.m4a';
|
||||
filePath = '${directory.path}/$fileName';
|
||||
|
||||
// Define the configuration for the recording
|
||||
|
||||
@@ -12,6 +12,14 @@ import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
||||
import 'package:Intaleq/views/widgets/my_textField.dart';
|
||||
|
||||
import '../../constant/style.dart';
|
||||
import 'package:Intaleq/controller/home/map/map_socket_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/map_engine_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/location_search_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/nearby_drivers_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ui_interactions_controller.dart';
|
||||
import 'package:Intaleq/controller/home/menu_controller.dart';
|
||||
import 'package:Intaleq/controller/home/points_for_rider_controller.dart';
|
||||
|
||||
class LogOutController extends GetxController {
|
||||
TextEditingController checkTxtController = TextEditingController();
|
||||
@@ -110,6 +118,15 @@ class LogOutController extends GetxController {
|
||||
box.remove(BoxName.accountIdStripeConnect);
|
||||
box.remove(BoxName.passengerWalletTotal);
|
||||
box.remove(BoxName.isVerified);
|
||||
Get.delete<MapSocketController>(force: true);
|
||||
Get.delete<MapEngineController>(force: true);
|
||||
Get.delete<LocationSearchController>(force: true);
|
||||
Get.delete<NearbyDriversController>(force: true);
|
||||
Get.delete<RideLifecycleController>(force: true);
|
||||
Get.delete<UiInteractionsController>(force: true);
|
||||
Get.delete<MyMenuController>(force: true);
|
||||
Get.delete<CRUD>(force: true);
|
||||
Get.delete<WayPointController>(force: true);
|
||||
Get.offAll(OnBoardingPage());
|
||||
},
|
||||
child: Text(
|
||||
|
||||
@@ -76,9 +76,11 @@ class ImageController extends GetxController {
|
||||
length,
|
||||
filename: basename(file.path),
|
||||
);
|
||||
final String fingerPrint = box.read(BoxName.deviceFpEncrypted)?.toString() ?? '';
|
||||
request.headers.addAll({
|
||||
'Authorization':
|
||||
'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}'
|
||||
'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}',
|
||||
'X-Device-FP': fingerPrint,
|
||||
});
|
||||
// Set the file name to the driverID
|
||||
request.files.add(
|
||||
|
||||
89
lib/controller/home/compare.sh
Normal file
89
lib/controller/home/compare.sh
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
|
||||
ORIG_FILE="lib/controller/home/map_passenger_controller.dart"
|
||||
ALL_FILES="lib/controller/home/map/location_search_controller.dart lib/controller/home/map/map_engine_controller.dart lib/controller/home/map/map_screen_binding.dart lib/controller/home/map/map_socket_controller.dart lib/controller/home/map/nearby_drivers_controller.dart lib/controller/home/map/ride_lifecycle_controller.dart lib/controller/home/map/ui_interactions_controller.dart"
|
||||
|
||||
echo "Extracting methods from original controller..."
|
||||
# Methods typically start with spaces and have patterns like:
|
||||
# returnType methodName( or methodName(
|
||||
# Let's extract words that precede ( on lines that don't start with keywords (if, for, while, switch, catch, etc.)
|
||||
# We will use awk to parse.
|
||||
METHODS=$(cat "$ORIG_FILE" | awk '
|
||||
# Skip single-line comments
|
||||
/\/\// { next }
|
||||
# Skip imports and class declarations
|
||||
/import/ || /class/ { next }
|
||||
# Find lines with "("
|
||||
/\(/ {
|
||||
# Replace anything inside parentheses and curly braces to simplify
|
||||
gsub(/\(.*\)/, "()")
|
||||
# Find word before "()"
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i ~ /[a-zA-Z0-9_]+\(\)/) {
|
||||
name = $i
|
||||
sub(/\(\)/, "", name)
|
||||
# Remove any leading modifiers like async, Future, static, etc.
|
||||
# Only keep valid identifiers that are not control keywords
|
||||
if (name !~ /^(if|for|while|switch|catch|super|await|print|assert|dynamic|void|return|with|override|get|set|else|try|final|const|var|late|static|factory|new|abstract|covariant|external|operator|part|required|typedef|yield)$/ && name ~ /^[a-zA-Z_][a-zA-Z0-9_]*$/) {
|
||||
print name
|
||||
}
|
||||
}
|
||||
}
|
||||
}' | sort -u)
|
||||
|
||||
echo "Extracting fields/variables from original controller..."
|
||||
# Fields are usually declared inside the class at the beginning of lines or indented.
|
||||
# e.g., RxBool isSearching = false.obs; or String? rideId;
|
||||
VARS=$(cat "$ORIG_FILE" | awk '
|
||||
/\/\// { next }
|
||||
/import/ || /class/ { next }
|
||||
# Lines ending with ";" or containing "=" followed by ";"
|
||||
/;/ {
|
||||
# Extract words that look like declarations.
|
||||
# We look for typical type names or var/final followed by variable name
|
||||
for (i = 1; i < NF; i++) {
|
||||
if ($i ~ /^(var|final|const|late|RxBool|RxInt|RxDouble|RxString|RxList|RxMap|RxSet|Rx|String|int|double|bool|List|Map|Set|Timer|LatLng|Position|IntaleqMapController)$/) {
|
||||
# The next field might be the variable name, or it might have a type like String?
|
||||
name = $(i+1)
|
||||
# Remove trailing ?, ;, =
|
||||
sub(/\?/, "", name)
|
||||
sub(/;/, "", name)
|
||||
sub(/=/, "", name)
|
||||
if (name ~ /^[a-zA-Z_][a-zA-Z0-9_]*$/) {
|
||||
print name
|
||||
}
|
||||
}
|
||||
}
|
||||
}' | sort -u)
|
||||
|
||||
echo "Checking split files for methods..."
|
||||
echo "--- MISSING METHODS ---"
|
||||
MISSING_METHODS_COUNT=0
|
||||
# Create a temporary file with all split contents to search efficiently
|
||||
cat $ALL_FILES > lib/controller/home/temp_split_combined.txt
|
||||
|
||||
for method in $METHODS; do
|
||||
# Search for this method name as a whole word in split controllers
|
||||
FOUND=$(grep -w "$method" lib/controller/home/temp_split_combined.txt 2>/dev/null)
|
||||
if [ -z "$FOUND" ]; then
|
||||
echo " - $method"
|
||||
MISSING_METHODS_COUNT=$((MISSING_METHODS_COUNT+1))
|
||||
fi
|
||||
done
|
||||
echo "Total missing methods: $MISSING_METHODS_COUNT"
|
||||
|
||||
echo ""
|
||||
echo "Checking split files for variables/fields..."
|
||||
echo "--- MISSING VARIABLES ---"
|
||||
MISSING_VARS_COUNT=0
|
||||
for var in $VARS; do
|
||||
FOUND=$(grep -w "$var" lib/controller/home/temp_split_combined.txt 2>/dev/null)
|
||||
if [ -z "$FOUND" ]; then
|
||||
echo " - $var"
|
||||
MISSING_VARS_COUNT=$((MISSING_VARS_COUNT+1))
|
||||
fi
|
||||
done
|
||||
echo "Total missing variables: $MISSING_VARS_COUNT"
|
||||
|
||||
# Clean up temp file
|
||||
rm lib/controller/home/temp_split_combined.txt
|
||||
104
lib/controller/home/compare_precise.py
Normal file
104
lib/controller/home/compare_precise.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import sys
|
||||
import re
|
||||
|
||||
def parse_stream(stream_text):
|
||||
# Splits the stream by our custom file delimiters
|
||||
files = {}
|
||||
parts = re.split(r'=== FILE: (.*?) ===\n', stream_text)
|
||||
|
||||
# The first part is the original monolithic file
|
||||
if parts:
|
||||
files['original'] = parts[0]
|
||||
|
||||
for i in range(1, len(parts), 2):
|
||||
filename = parts[i]
|
||||
content = parts[i+1] if i+1 < len(parts) else ""
|
||||
files[filename] = content
|
||||
|
||||
return files
|
||||
|
||||
def strip_comments(text):
|
||||
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
|
||||
text = re.sub(r'//.*', '', text)
|
||||
return text
|
||||
|
||||
def extract_declarations(text):
|
||||
clean = strip_comments(text)
|
||||
|
||||
# Matches method/function declarations inside a class in Dart
|
||||
# e.g., void myMethod(..., Future<void> myMethod(..., myMethod(..., get myProp, set myProp
|
||||
# We look for word followed by ( or get/set followed by word.
|
||||
method_decl_pattern = re.compile(
|
||||
r'(?:[a-zA-Z0-9_<>\?\[\]]+(?:\s+[a-zA-Z0-9_<>\?\[\]]+)*\s+)?([a-zA-Z0-9_]+)\s*\([^\)]*\)\s*(?:async)?\s*(?:=>|\{)'
|
||||
)
|
||||
|
||||
methods = set()
|
||||
for match in method_decl_pattern.finditer(clean):
|
||||
method_name = match.group(1)
|
||||
if method_name not in keywords and not method_name.isdigit():
|
||||
methods.add(method_name)
|
||||
|
||||
# Also extract getters and setters
|
||||
getset_pattern = re.compile(r'\b(?:get|set)\s+([a-zA-Z0-9_]+)\b')
|
||||
for match in getset_pattern.finditer(clean):
|
||||
name = match.group(1)
|
||||
if name not in keywords:
|
||||
methods.add(name)
|
||||
|
||||
# Extract variables/fields declarations
|
||||
var_decl_pattern = re.compile(
|
||||
r'\b(?:var|final|const|late|RxBool|RxInt|RxDouble|RxString|RxList|RxMap|RxSet|Rx|String|int|double|bool|List|Map|Set|Timer|LatLng|Position|IntaleqMapController)\??\s+([a-zA-Z0-9_]+)\b'
|
||||
)
|
||||
|
||||
variables = set()
|
||||
for match in var_decl_pattern.finditer(clean):
|
||||
var_name = match.group(1)
|
||||
if var_name not in keywords and not var_name.isdigit():
|
||||
variables.add(var_name)
|
||||
|
||||
return methods, variables
|
||||
|
||||
keywords = {
|
||||
'if', 'for', 'while', 'switch', 'catch', 'super', 'await', 'print',
|
||||
'assert', 'dynamic', 'void', 'return', 'with', 'override', 'get', 'set',
|
||||
'class', 'import', 'extends', 'implements', 'mixin', 'this', 'else', 'try',
|
||||
'final', 'const', 'var', 'late', 'static', 'factory', 'new', 'abstract',
|
||||
'covariant', 'external', 'operator', 'part', 'required', 'typedef', 'yield'
|
||||
}
|
||||
|
||||
def main():
|
||||
stream_text = sys.stdin.read()
|
||||
files = parse_stream(stream_text)
|
||||
|
||||
orig_content = files.get('original', '')
|
||||
split_contents = {k: v for k, v in files.items() if k != 'original'}
|
||||
|
||||
orig_methods, orig_vars = extract_declarations(orig_content)
|
||||
|
||||
# Combined declarations in split files
|
||||
split_methods = set()
|
||||
split_vars = set()
|
||||
for filename, content in split_contents.items():
|
||||
m, v = extract_declarations(content)
|
||||
split_methods.update(m)
|
||||
split_vars.update(v)
|
||||
|
||||
missing_methods = sorted(orig_methods - split_methods)
|
||||
missing_vars = sorted(orig_vars - split_vars)
|
||||
|
||||
print("--- PRECISE MISSING METHODS ---")
|
||||
print(f"Total original methods/getters/setters: {len(orig_methods)}")
|
||||
print(f"Total defined in split controllers: {len(split_methods)}")
|
||||
print(f"Total missing: {len(missing_methods)}")
|
||||
for m in missing_methods:
|
||||
print(f" - {m}")
|
||||
|
||||
print("\n--- PRECISE MISSING VARIABLES/FIELDS ---")
|
||||
print(f"Total original variables: {len(orig_vars)}")
|
||||
print(f"Total defined in split controllers: {len(split_vars)}")
|
||||
print(f"Total missing: {len(missing_vars)}")
|
||||
for v in missing_vars:
|
||||
print(f" - {v}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
103
lib/controller/home/comparison_results.txt
Normal file
103
lib/controller/home/comparison_results.txt
Normal file
@@ -0,0 +1,103 @@
|
||||
Extracting methods from original controller...
|
||||
Extracting fields/variables from original controller...
|
||||
Checking split files for methods...
|
||||
--- MISSING METHODS ---
|
||||
- _applyLowEndModeIfNeeded
|
||||
- _buildOsrmWaypointCoords
|
||||
- _calculateDistance
|
||||
- _checkAndRecalculateIfDeviated
|
||||
- _fillDriverDataLocally
|
||||
- _haversineKm
|
||||
- _initMinimalIcons
|
||||
- _initializePolygons
|
||||
- _isActiveRideState
|
||||
- _kmToLatDelta
|
||||
- _kmToLngDelta
|
||||
- _onDriverArrivedWithSocket
|
||||
- _onRideCancelledWithSocket
|
||||
- _onRideStartedWithSocket
|
||||
- _relevanceScore
|
||||
- _restorePolyline
|
||||
- _stageNiceToHave
|
||||
- _stagePricingAndState
|
||||
- _startMasterTimer
|
||||
- _startMasterTimerWithInterval
|
||||
- _startPollingFallback
|
||||
- _stopDriverLocationPolling
|
||||
- _updateDriverMarker
|
||||
- cancelRide
|
||||
- detectPerfMode
|
||||
- getAIKey
|
||||
- getMapPointsForAllMethods
|
||||
- getPassengerLocationUniversity
|
||||
- handleActiveRideOnStartup
|
||||
- isDriversDataValid
|
||||
- onChangedPassengerCount
|
||||
- onChangedPassengersChoose
|
||||
- showDrawingBottomSheet
|
||||
- showNoDriversDialog
|
||||
- startSearchingTimer
|
||||
Total missing methods: 35
|
||||
|
||||
Checking split files for variables/fields...
|
||||
--- MISSING VARIABLES ---
|
||||
- _isStateProcessing
|
||||
- _isUsingFallback
|
||||
- _maxReconnectAttempts
|
||||
- apiDistanceMeters
|
||||
- c
|
||||
- carInfo
|
||||
- carsOrder
|
||||
- coordDestination
|
||||
- currentCarType
|
||||
- currentDriverLocation
|
||||
- currentLocationOfDrivers
|
||||
- currentRideId
|
||||
- currentTimeSearchingCaptainWindow
|
||||
- dInfo
|
||||
- dLat
|
||||
- datadriverCarsLocationToPassengerAfterApplied
|
||||
- distanceOfTrip
|
||||
- driverCarPlate
|
||||
- driverLocationToPassenger
|
||||
- driverOrderStatus
|
||||
- durationByPassenger
|
||||
- endLocation
|
||||
- fName
|
||||
- finalReason
|
||||
- headingList
|
||||
- increaseFeeFormKey
|
||||
- isDriversTokensSend
|
||||
- isFirstWaypoint
|
||||
- isInUniversity
|
||||
- isSaaSRequest
|
||||
- kmInDegree
|
||||
- lName
|
||||
- latDest
|
||||
- latestPosition
|
||||
- lngDest
|
||||
- lowPerf
|
||||
- messagesFormKey
|
||||
- originCoords
|
||||
- pLower
|
||||
- passengerLocationStringUnvirsity
|
||||
- previousLocationOfDrivers
|
||||
- progressTimerRideBeginVip
|
||||
- qLower
|
||||
- rLat1
|
||||
- rLat2
|
||||
- ram
|
||||
- rideData
|
||||
- sdk
|
||||
- selectedPassengerCount
|
||||
- startLng
|
||||
- startLocation
|
||||
- stringElapsedTimeRideBegin
|
||||
- tax
|
||||
- totalPassengerBalashDiscount
|
||||
- totalPassengerComfortDiscount
|
||||
- totalPassengerElectricDiscount
|
||||
- totalPassengerLadyDiscount
|
||||
- totalPassengerRaihGaiDiscount
|
||||
- totalPassengerSpeedDiscount
|
||||
Total missing variables: 59
|
||||
15
lib/controller/home/map/car_location.dart
Normal file
15
lib/controller/home/map/car_location.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
class CarLocation {
|
||||
final String id;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final double distance;
|
||||
final double duration;
|
||||
|
||||
CarLocation({
|
||||
required this.id,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
this.distance = 10000,
|
||||
this.duration = 10000,
|
||||
});
|
||||
}
|
||||
1052
lib/controller/home/map/location_search_controller.dart
Normal file
1052
lib/controller/home/map/location_search_controller.dart
Normal file
File diff suppressed because it is too large
Load Diff
809
lib/controller/home/map/map_engine_controller.dart
Normal file
809
lib/controller/home/map/map_engine_controller.dart
Normal file
@@ -0,0 +1,809 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' show cos, max, min, pi, pow, sqrt;
|
||||
import 'dart:typed_data';
|
||||
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:image/image.dart' as img;
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
// contains global 'box'
|
||||
import '../../../print.dart';
|
||||
import '../../../views/home/map_widget.dart/cancel_raide_page.dart';
|
||||
import 'location_search_controller.dart';
|
||||
import 'nearby_drivers_controller.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import '../points_for_rider_controller.dart';
|
||||
import '../../../constant/univeries_polygon.dart';
|
||||
|
||||
class MapEngineController extends GetxController {
|
||||
IntaleqMapController? mapController;
|
||||
bool isStyleLoaded = false;
|
||||
bool isIconsLoaded = false;
|
||||
|
||||
Set<Marker> markers = {};
|
||||
Set<Polyline> polyLines = {};
|
||||
List<LatLng> polylineCoordinates = [];
|
||||
Set<Polygon> polygons = {};
|
||||
Set<Circle> circles = {};
|
||||
|
||||
LatLngBounds? lastComputedBounds;
|
||||
bool mapType = false;
|
||||
bool mapTrafficON = false;
|
||||
bool isMarkersShown = false;
|
||||
|
||||
String markerIcon = "marker_icon";
|
||||
String tripIcon = "trip_icon";
|
||||
String startIcon = "start_icon";
|
||||
String endIcon = "end_icon";
|
||||
String carIcon = "car_icon";
|
||||
String motoIcon = "moto_icon";
|
||||
String ladyIcon = "lady_icon";
|
||||
|
||||
double height = 150;
|
||||
double heightMenu = 0;
|
||||
double widthMenu = 0;
|
||||
double heightPickerContainer = 90;
|
||||
double heightPointsPageForRider = 0;
|
||||
double mainBottomMenuMapHeight = Get.height * .2;
|
||||
double wayPointSheetHeight = 0;
|
||||
bool heightMenuBool = false;
|
||||
bool isPickerShown = false;
|
||||
bool isPointsPageForRider = false;
|
||||
bool isBottomSheetShown = false;
|
||||
bool reloadStartApp = false;
|
||||
bool isCancelRidePageShown = false;
|
||||
bool isCashConfirmPageShown = false;
|
||||
bool isPaymentMethodPageShown = false;
|
||||
bool isRideFinished = false;
|
||||
bool rideConfirm = false;
|
||||
bool isMainBottomMenuMap = true;
|
||||
|
||||
bool isWayPointSheet = false;
|
||||
bool isWayPointStopsSheet = false;
|
||||
bool isWayPointStopsSheetUtilGetMap = false;
|
||||
double heightBottomSheetShown = 0;
|
||||
double cashConfirmPageShown = 250;
|
||||
double widthMapTypeAndTraffic = 50;
|
||||
double paymentPageShown = Get.height * .6;
|
||||
|
||||
bool isAnotherOreder = false;
|
||||
bool isWhatsAppOrder = false;
|
||||
|
||||
Map<String, Timer> _animationTimers = {};
|
||||
final int updateIntervalMs = 100;
|
||||
final double minMovementThreshold = 1.0;
|
||||
|
||||
void onMapCreated(IntaleqMapController controller) {
|
||||
mapController = controller;
|
||||
update();
|
||||
}
|
||||
|
||||
void onStyleLoaded() async {
|
||||
Log.print('🗺️ Intaleq Map Style Loaded. Initializing...');
|
||||
isStyleLoaded = true;
|
||||
await _loadMapIcons();
|
||||
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
Get.find<RideLifecycleController>().reinit();
|
||||
|
||||
if (mapController != null) {
|
||||
if (markers.isNotEmpty && lastComputedBounds != null) {
|
||||
await _safeAnimateCameraBounds(lastComputedBounds);
|
||||
} else {
|
||||
mapController!.animateCamera(
|
||||
CameraUpdate.newLatLng(locationSearch.passengerLocation),
|
||||
);
|
||||
}
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> _safeAnimateCameraBounds(LatLngBounds? bounds,
|
||||
{double left = 60,
|
||||
double top = 60,
|
||||
double right = 60,
|
||||
double bottom = 60}) async {
|
||||
if (bounds == null || mapController == null) return;
|
||||
|
||||
try {
|
||||
if (bounds.northeast.latitude == bounds.southwest.latitude &&
|
||||
bounds.northeast.longitude == bounds.southwest.longitude) {
|
||||
Log.print(
|
||||
'⚠️ _safeAnimateCameraBounds: Bounds are a single point, zooming to point instead.');
|
||||
await mapController
|
||||
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 15));
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
await mapController?.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(
|
||||
bounds,
|
||||
left: left,
|
||||
top: top,
|
||||
right: right,
|
||||
bottom: bottom,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.print('❌ _safeAnimateCameraBounds CRASH PREVENTED: $e');
|
||||
try {
|
||||
await mapController
|
||||
?.animateCamera(CameraUpdate.newLatLngZoom(bounds.northeast, 14));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMapIcons() async {
|
||||
isIconsLoaded = false;
|
||||
for (int i = 0; i < 15; i++) {
|
||||
if (mapController != null && isStyleLoaded) break;
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
if (mapController == null || !isStyleLoaded) {
|
||||
Log.print(
|
||||
'⚠️ _loadMapIcons: mapController or style not ready. Icons may not load.');
|
||||
}
|
||||
|
||||
await _addMapImage(startIcon, 'assets/images/A.png');
|
||||
await _addMapImage(endIcon, 'assets/images/b.png');
|
||||
await _addMapImage(carIcon, 'assets/images/car.png');
|
||||
await _addMapImage(motoIcon, 'assets/images/moto.png');
|
||||
await _addMapImage(ladyIcon, 'assets/images/lady.png');
|
||||
await _addMapImage('picker_icon', 'assets/images/picker.png');
|
||||
await _addMapImage('orange_marker', 'assets/images/moto1.png');
|
||||
await _addMapImage('violet_marker', 'assets/images/lady1.png');
|
||||
|
||||
isIconsLoaded = true;
|
||||
markers = markers.map((m) => m.copyWith()).toSet();
|
||||
update();
|
||||
|
||||
if (Get.isRegistered<NearbyDriversController>()) {
|
||||
Get.find<NearbyDriversController>()
|
||||
.getCarsLocationByPassengerAndReloadMarker();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addMapImage(String id, String path) async {
|
||||
try {
|
||||
final ByteData bytes = await rootBundle.load(path);
|
||||
final size = _getImageSize(id);
|
||||
if (size != null && (id == carIcon || id == motoIcon || id == ladyIcon)) {
|
||||
final resized = await _resizeImage(bytes.buffer.asUint8List(), size);
|
||||
await mapController?.addImage(id, resized);
|
||||
Log.print(
|
||||
'delimited: successfully added resized map image: $id (${size}x${size})');
|
||||
} else {
|
||||
await mapController?.addImage(id, bytes.buffer.asUint8List());
|
||||
Log.print('delimited: successfully added map image: $id');
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('❌ Error loading map icon $id: $e');
|
||||
}
|
||||
}
|
||||
|
||||
int? _getImageSize(String id) {
|
||||
if (id == carIcon || id == motoIcon || id == ladyIcon) return 120;
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Uint8List> _resizeImage(Uint8List bytes, int size) async {
|
||||
return await compute((Uint8List data) {
|
||||
final image = img.decodeImage(data);
|
||||
if (image == null) return data;
|
||||
final resized = img.copyResize(image, width: size, height: size);
|
||||
return Uint8List.fromList(img.encodePng(resized));
|
||||
}, bytes);
|
||||
}
|
||||
|
||||
void clearPolyline() {
|
||||
polyLines.clear();
|
||||
update();
|
||||
}
|
||||
|
||||
LatLngBounds calculateBounds(double lat, double lng, double radiusInMeters) {
|
||||
const double earthRadius = 6378137.0;
|
||||
|
||||
double latDelta = (radiusInMeters / earthRadius) * (180 / pi);
|
||||
double lngDelta =
|
||||
(radiusInMeters / (earthRadius * cos(pi * lat / 180))) * (180 / pi);
|
||||
|
||||
double minLat = lat - latDelta;
|
||||
double maxLat = lat + latDelta;
|
||||
|
||||
double minLng = lng - lngDelta;
|
||||
double maxLng = lng + lngDelta;
|
||||
|
||||
minLat = max(-90.0, minLat);
|
||||
maxLat = min(90.0, maxLat);
|
||||
|
||||
minLng = (minLng + 180) % 360 - 180;
|
||||
maxLng = (maxLng + 180) % 360 - 180;
|
||||
|
||||
if (minLng > maxLng) {
|
||||
double temp = minLng;
|
||||
minLng = maxLng;
|
||||
maxLng = temp;
|
||||
}
|
||||
|
||||
return LatLngBounds(
|
||||
southwest: LatLng(minLat, minLng),
|
||||
northeast: LatLng(maxLat, maxLng),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> playRouteAnimation(
|
||||
List<LatLng> coords, LatLngBounds? bounds) async {
|
||||
const List<Color> segmentColors = [
|
||||
Color(0xFF109642), // Green
|
||||
Color(0xFFF59E0B), // Amber
|
||||
Color(0xFF7C3AED), // Purple
|
||||
Color(0xFFEF4444), // Red
|
||||
];
|
||||
|
||||
Set<Polyline> newPolylines = {};
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (locationSearch.activeMenuWaypointCount > 0) {
|
||||
List<int> splitIndices = [];
|
||||
for (int w = 0; w < locationSearch.activeMenuWaypointCount; w++) {
|
||||
final wp = locationSearch.menuWaypoints[w];
|
||||
if (wp == null) continue;
|
||||
int bestIdx = 0;
|
||||
double bestDist = double.infinity;
|
||||
for (int j = 0; j < coords.length; j++) {
|
||||
final dx = coords[j].latitude - wp.latitude;
|
||||
final dy = coords[j].longitude - wp.longitude;
|
||||
final d = dx * dx + dy * dy;
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
bestIdx = j;
|
||||
}
|
||||
}
|
||||
splitIndices.add(bestIdx);
|
||||
}
|
||||
splitIndices.sort();
|
||||
|
||||
List<int> boundaries = [0, ...splitIndices, coords.length - 1];
|
||||
for (int s = 0; s < boundaries.length - 1; s++) {
|
||||
int from = boundaries[s];
|
||||
int to = boundaries[s + 1] + 1;
|
||||
if (to > coords.length) to = coords.length;
|
||||
if (from >= to - 1) continue;
|
||||
final segCoords = coords.sublist(from, to);
|
||||
if (segCoords.length < 2) continue;
|
||||
final color = segmentColors[s % segmentColors.length];
|
||||
|
||||
newPolylines.add(Polyline(
|
||||
polylineId: PolylineId('segment_$s'),
|
||||
points: segCoords,
|
||||
color: color,
|
||||
width: 6,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
newPolylines.add(Polyline(
|
||||
polylineId: const PolylineId('route_primary'),
|
||||
points: coords,
|
||||
color: AppColor.primaryColor,
|
||||
width: 6,
|
||||
));
|
||||
}
|
||||
|
||||
polyLines = newPolylines;
|
||||
update();
|
||||
|
||||
Log.print(
|
||||
'🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map');
|
||||
|
||||
if (bounds != null) {
|
||||
await _safeAnimateCameraBounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
void _fitCameraToPoints(LatLng p1, LatLng p2) async {
|
||||
if (mapController == null) return;
|
||||
|
||||
if (p1.latitude == p2.latitude && p1.longitude == p2.longitude) {
|
||||
try {
|
||||
mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 17));
|
||||
} catch (e) {
|
||||
Log.print("Error animating to single point: $e");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
double minLat = min(p1.latitude, p2.latitude);
|
||||
double maxLat = max(p1.latitude, p2.latitude);
|
||||
double minLng = min(p1.longitude, p2.longitude);
|
||||
double maxLng = max(p1.longitude, p2.longitude);
|
||||
|
||||
if ((maxLat - minLat).abs() < 0.002 && (maxLng - minLng).abs() < 0.002) {
|
||||
try {
|
||||
mapController?.animateCamera(CameraUpdate.newLatLngZoom(p1, 16));
|
||||
} catch (e) {
|
||||
Log.print("Error animating to single point: $e");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
double padding = 50.0;
|
||||
|
||||
try {
|
||||
await mapController?.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(
|
||||
LatLngBounds(
|
||||
southwest: LatLng(minLat, minLng),
|
||||
northeast: LatLng(maxLat, maxLng),
|
||||
),
|
||||
left: padding,
|
||||
top: padding,
|
||||
right: padding,
|
||||
bottom: padding,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.print("Error animating bounds: $e");
|
||||
try {
|
||||
LatLng center = LatLng((minLat + maxLat) / 2, (minLng + maxLng) / 2);
|
||||
mapController?.animateCamera(CameraUpdate.newLatLngZoom(center, 14));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
void fitCameraToPoints(LatLng p1, LatLng p2) {
|
||||
_fitCameraToPoints(p1, p2);
|
||||
}
|
||||
|
||||
void clearMarkersExceptStartEndAndDriver() {
|
||||
const String currentDriverMarkerId = 'assigned_driver_marker';
|
||||
markers.removeWhere((marker) {
|
||||
String id = marker.markerId.value;
|
||||
if (id == 'start') return false;
|
||||
if (id == 'end') return false;
|
||||
if (id == currentDriverMarkerId) return false;
|
||||
return true;
|
||||
});
|
||||
update();
|
||||
}
|
||||
|
||||
void clearMarkersExceptStartEnd() {
|
||||
markers.removeWhere((marker) {
|
||||
String id = marker.markerId.value;
|
||||
return id != 'start' && id != 'end';
|
||||
});
|
||||
update();
|
||||
}
|
||||
|
||||
void _updateMarkerPosition(
|
||||
LatLng newPosition, double newHeading, String icon) {
|
||||
const String markerId = 'driverToPassengers';
|
||||
final mId = MarkerId(markerId);
|
||||
final existingMarker = markers.cast<Marker?>().firstWhere(
|
||||
(m) => m?.markerId == mId,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (existingMarker != null) {
|
||||
_smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon);
|
||||
} else {
|
||||
markers = {
|
||||
...markers,
|
||||
Marker(
|
||||
markerId: mId,
|
||||
position: newPosition,
|
||||
rotation: newHeading,
|
||||
icon: InlqBitmap.fromStyleImage(icon),
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
),
|
||||
};
|
||||
update();
|
||||
}
|
||||
mapController?.animateCamera(CameraUpdate.newLatLng(newPosition));
|
||||
}
|
||||
|
||||
void updateMarkerPosition(
|
||||
LatLng newPosition, double newHeading, String icon) {
|
||||
_updateMarkerPosition(newPosition, newHeading, icon);
|
||||
}
|
||||
|
||||
void _smoothlyUpdateMarker(
|
||||
Marker oldMarker, LatLng newPosition, double newHeading, String icon) {
|
||||
double distance = Geolocator.distanceBetween(
|
||||
oldMarker.position.latitude,
|
||||
oldMarker.position.longitude,
|
||||
newPosition.latitude,
|
||||
newPosition.longitude);
|
||||
|
||||
if (distance < 2.0) return;
|
||||
|
||||
final MarkerId markerIdKey = oldMarker.markerId;
|
||||
_animationTimers[markerIdKey.value]?.cancel();
|
||||
|
||||
int ticks = 0;
|
||||
const int totalSteps = 20;
|
||||
const int stepDuration = 50;
|
||||
|
||||
double latStep =
|
||||
(newPosition.latitude - oldMarker.position.latitude) / totalSteps;
|
||||
double lngStep =
|
||||
(newPosition.longitude - oldMarker.position.longitude) / totalSteps;
|
||||
double headingStep = (newHeading - oldMarker.rotation) / totalSteps;
|
||||
|
||||
LatLng currentPos = oldMarker.position;
|
||||
double currentHeading = oldMarker.rotation;
|
||||
|
||||
_animationTimers[markerIdKey.value] =
|
||||
Timer.periodic(const Duration(milliseconds: stepDuration), (timer) {
|
||||
ticks++;
|
||||
|
||||
currentPos =
|
||||
LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep);
|
||||
currentHeading += headingStep;
|
||||
|
||||
final updatedMarker = oldMarker.copyWith(
|
||||
position: currentPos,
|
||||
rotation: currentHeading,
|
||||
icon: InlqBitmap.fromStyleImage(icon),
|
||||
);
|
||||
|
||||
markers = {
|
||||
...markers.where((m) => m.markerId != markerIdKey),
|
||||
updatedMarker,
|
||||
};
|
||||
|
||||
if (mapController != null) {
|
||||
mapController!.animateCamera(CameraUpdate.newLatLng(currentPos));
|
||||
}
|
||||
|
||||
update();
|
||||
|
||||
if (ticks >= totalSteps) {
|
||||
timer.cancel();
|
||||
_animationTimers.remove(markerIdKey.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// تحديث موقع العلامة (Marker) واتجاهها بسلاسة على الخريطة.
|
||||
// تحسب الدالة المسافة بين الموقع الحالي والجديد؛ وإذا كانت أكبر من مترين،
|
||||
// تقوم بتقسيم الحركة والدوران إلى 20 خطوة متباعدة بـ 50 مللي ثانية (إجمالي ثانية واحدة).
|
||||
// يتم تحديث موضع العلامة وتحريك الكاميرا تدريجياً لتبدو حركة السيارة انسيابية.
|
||||
void smoothlyUpdateMarker(
|
||||
Marker oldMarker, LatLng newPosition, double newHeading, String icon) {
|
||||
_smoothlyUpdateMarker(oldMarker, newPosition, newHeading, icon);
|
||||
}
|
||||
|
||||
void changeBottomSheetShown({bool? forceValue}) {
|
||||
if (forceValue != null) {
|
||||
isBottomSheetShown = forceValue;
|
||||
} else {
|
||||
isBottomSheetShown = !isBottomSheetShown;
|
||||
}
|
||||
heightBottomSheetShown = isBottomSheetShown == true ? 250 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeCashConfirmPageShown() {
|
||||
isCashConfirmPageShown = !isCashConfirmPageShown;
|
||||
final rideLife = Get.find<RideLifecycleController>();
|
||||
rideLife.isCashSelectedBeforeConfirmRide = true;
|
||||
cashConfirmPageShown = isCashConfirmPageShown == true ? 250 : 0;
|
||||
update();
|
||||
rideLife.update();
|
||||
}
|
||||
|
||||
void changePaymentMethodPageShown() {
|
||||
isPaymentMethodPageShown = !isPaymentMethodPageShown;
|
||||
paymentPageShown = isPaymentMethodPageShown == true ? Get.height * .6 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeMapType() {
|
||||
mapType = !mapType;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeMapTraffic() {
|
||||
mapTrafficON = !mapTrafficON;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeisAnotherOreder(bool val) {
|
||||
isAnotherOreder = val;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeIsWhatsAppOrder(bool val) {
|
||||
isWhatsAppOrder = val;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeCancelRidePageShow() {
|
||||
showCancelRideBottomSheet();
|
||||
isCancelRidePageShown = !isCancelRidePageShown;
|
||||
update();
|
||||
if (Get.isRegistered<RideLifecycleController>()) {
|
||||
Get.find<RideLifecycleController>().update();
|
||||
}
|
||||
}
|
||||
|
||||
void getDrawerMenu() {
|
||||
heightMenuBool = !heightMenuBool;
|
||||
widthMapTypeAndTraffic = heightMenuBool == true ? 0 : 50;
|
||||
heightMenu = heightMenuBool == true ? 80 : 0;
|
||||
widthMenu = heightMenuBool == true ? 110 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeMainBottomMenuMap() {
|
||||
if (isWayPointStopsSheetUtilGetMap == true) {
|
||||
changeWayPointSheet();
|
||||
} else {
|
||||
isMainBottomMenuMap = !isMainBottomMenuMap;
|
||||
mainBottomMenuMapHeight =
|
||||
isMainBottomMenuMap == true ? Get.height * .22 : Get.height * .6;
|
||||
isWayPointSheet = false;
|
||||
if (heightMenuBool == true) {
|
||||
getDrawerMenu();
|
||||
}
|
||||
Get.find<RideLifecycleController>().initilizeGetStorage();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void downPoints() {
|
||||
if (Get.find<WayPointController>().wayPoints.length < 2) {
|
||||
isWayPointStopsSheetUtilGetMap = false;
|
||||
isWayPointSheet = false;
|
||||
wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0;
|
||||
update();
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void changeWayPointSheet() {
|
||||
isWayPointSheet = !isWayPointSheet;
|
||||
wayPointSheetHeight = isWayPointSheet == false ? 0 : Get.height * .45;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeWayPointStopsSheet() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPointIndex > -1) {
|
||||
isWayPointStopsSheet = true;
|
||||
isWayPointStopsSheetUtilGetMap = true;
|
||||
}
|
||||
isWayPointStopsSheet = !isWayPointStopsSheet;
|
||||
wayPointSheetHeight = isWayPointStopsSheet ? Get.height * .45 : 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changeHeightPlaces() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.placesDestination.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightStartPlaces() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.placesStart.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlacesAll(int index) {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.placeListResponseAll[index].isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlaces1() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPoint1.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlaces2() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPoint2.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlaces3() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPoint3.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void changeHeightPlaces4() {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
if (locationSearch.wayPoint4.isEmpty) {
|
||||
height = 0;
|
||||
update();
|
||||
} else {
|
||||
height = 150;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void hidePlaces() {
|
||||
height = 0;
|
||||
update();
|
||||
}
|
||||
|
||||
void changePickerShown() {
|
||||
isPickerShown = !isPickerShown;
|
||||
heightPickerContainer = isPickerShown == true ? 150 : 90;
|
||||
update();
|
||||
}
|
||||
|
||||
void _initializePolygons() {
|
||||
List<List<LatLng>> universityPolygons =
|
||||
UniversitiesPolygons.universityPolygons;
|
||||
|
||||
for (int i = 0; i < universityPolygons.length; i++) {
|
||||
Polygon polygon = Polygon(
|
||||
polygonId: PolygonId('univ_$i'),
|
||||
points: universityPolygons[i],
|
||||
fillColor: Colors.blueAccent.withOpacity(0.2),
|
||||
strokeColor: Colors.blueAccent,
|
||||
strokeWidth: 2,
|
||||
);
|
||||
polygons.add(polygon);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void _applyLowEndModeIfNeeded() {
|
||||
// Placeholder comment from original
|
||||
}
|
||||
|
||||
Future<void> _initMinimalIcons() async {
|
||||
// Icons are loaded dynamically
|
||||
}
|
||||
|
||||
Future<void> _playRouteAnimation(
|
||||
List<LatLng> coords, LatLngBounds? bounds) async {
|
||||
const List<Color> segmentColors = [
|
||||
Color(0xFF109642), // Green
|
||||
Color(0xFFF59E0B), // Amber
|
||||
Color(0xFF7C3AED), // Purple
|
||||
Color(0xFFEF4444), // Red
|
||||
];
|
||||
|
||||
Set<Polyline> newPolylines = {};
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (locSearch.activeMenuWaypointCount > 0) {
|
||||
List<int> splitIndices = [];
|
||||
for (int w = 0; w < locSearch.activeMenuWaypointCount; w++) {
|
||||
final wp = locSearch.menuWaypoints[w];
|
||||
if (wp == null) continue;
|
||||
int bestIdx = 0;
|
||||
double bestDist = double.infinity;
|
||||
for (int j = 0; j < coords.length; j++) {
|
||||
final dx = coords[j].latitude - wp.latitude;
|
||||
final dy = coords[j].longitude - wp.longitude;
|
||||
final d = dx * dx + dy * dy;
|
||||
if (d < bestDist) {
|
||||
bestDist = d;
|
||||
bestIdx = j;
|
||||
}
|
||||
}
|
||||
splitIndices.add(bestIdx);
|
||||
}
|
||||
splitIndices.sort();
|
||||
|
||||
List<int> boundaries = [0, ...splitIndices, coords.length - 1];
|
||||
for (int s = 0; s < boundaries.length - 1; s++) {
|
||||
int from = boundaries[s];
|
||||
int to = boundaries[s + 1] + 1;
|
||||
if (to > coords.length) to = coords.length;
|
||||
if (from >= to - 1) continue;
|
||||
final segCoords = coords.sublist(from, to);
|
||||
if (segCoords.length < 2) continue;
|
||||
final color = segmentColors[s % segmentColors.length];
|
||||
|
||||
newPolylines.add(Polyline(
|
||||
polylineId: PolylineId('segment_$s'),
|
||||
points: segCoords,
|
||||
color: color,
|
||||
width: 6,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
newPolylines.add(Polyline(
|
||||
polylineId: const PolylineId('route_primary'),
|
||||
points: coords,
|
||||
color: AppColor.primaryColor,
|
||||
width: 6,
|
||||
));
|
||||
}
|
||||
|
||||
polyLines = newPolylines;
|
||||
update();
|
||||
|
||||
Log.print(
|
||||
'🗺️ Drawing ${markers.length} markers + ${polyLines.length} polylines on map');
|
||||
|
||||
update();
|
||||
|
||||
if (bounds != null) {
|
||||
await _safeAnimateCameraBounds(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
isPickerShown = false;
|
||||
isPointsPageForRider = false;
|
||||
isBottomSheetShown = false;
|
||||
isCancelRidePageShown = false;
|
||||
isCashConfirmPageShown = false;
|
||||
isPaymentMethodPageShown = false;
|
||||
isRideFinished = false;
|
||||
rideConfirm = false;
|
||||
isMainBottomMenuMap = true;
|
||||
|
||||
isWayPointSheet = false;
|
||||
isWayPointStopsSheet = false;
|
||||
isWayPointStopsSheetUtilGetMap = false;
|
||||
|
||||
heightBottomSheetShown = 0;
|
||||
mainBottomMenuMapHeight = Get.height * 0.22;
|
||||
wayPointSheetHeight = 0;
|
||||
|
||||
markers.clear();
|
||||
polyLines.clear();
|
||||
polylineCoordinates.clear();
|
||||
|
||||
_animationTimers.forEach((key, timer) => timer.cancel());
|
||||
_animationTimers.clear();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_animationTimers.forEach((key, timer) => timer.cancel());
|
||||
_animationTimers.clear();
|
||||
mapController = null;
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
25
lib/controller/home/map/map_screen_binding.dart
Normal file
25
lib/controller/home/map/map_screen_binding.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'map_socket_controller.dart';
|
||||
import 'map_engine_controller.dart';
|
||||
import 'location_search_controller.dart';
|
||||
import 'nearby_drivers_controller.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import 'ui_interactions_controller.dart';
|
||||
|
||||
class MapScreenBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
// 1. WebSocket Controller: Permanent and immediate
|
||||
Get.put(MapSocketController());
|
||||
|
||||
// 2. Core Controllers (initialized when the screen opens or on demand)
|
||||
Get.lazyPut(() => MapEngineController());
|
||||
Get.lazyPut(() => LocationSearchController());
|
||||
Get.lazyPut(() => NearbyDriversController());
|
||||
|
||||
// 3. Lifecycle and UI Interaction Controllers
|
||||
Get.lazyPut(() => RideLifecycleController());
|
||||
Get.lazyPut(() => UiInteractionsController(), fenix: true);
|
||||
}
|
||||
}
|
||||
326
lib/controller/home/map/map_socket_controller.dart
Normal file
326
lib/controller/home/map/map_socket_controller.dart
Normal file
@@ -0,0 +1,326 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:socket_io_client/socket_io_client.dart' as io_client;
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../main.dart'; // contains global 'box'
|
||||
import '../../../print.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import 'nearby_drivers_controller.dart';
|
||||
import 'map_engine_controller.dart';
|
||||
|
||||
class MapSocketController extends GetxController {
|
||||
late io_client.Socket socket;
|
||||
bool isSocketConnected = false;
|
||||
bool _isSocketInitialized = false;
|
||||
Timer? _heartbeatTimer;
|
||||
DateTime? _lastSocketLocationTime;
|
||||
int _socketLocationUpdatesCount = 0;
|
||||
Timer? _watchdogTimer;
|
||||
|
||||
DateTime? get lastDriverLocationTime => _lastSocketLocationTime;
|
||||
int get socketLocationUpdatesCount => _socketLocationUpdatesCount;
|
||||
|
||||
void initConnectionWithSocket() {
|
||||
if (isSocketConnected) return;
|
||||
|
||||
String passengerId = box.read(BoxName.passengerID).toString();
|
||||
Log.print("🔌 Initializing Socket for Passenger: $passengerId");
|
||||
|
||||
socket = io_client.io(
|
||||
AppLink.serverSocket,
|
||||
io_client.OptionBuilder()
|
||||
.setTransports(['websocket'])
|
||||
.disableAutoConnect()
|
||||
.setQuery({'id': passengerId})
|
||||
.setReconnectionAttempts(20)
|
||||
.setReconnectionDelay(2000)
|
||||
.setReconnectionDelayMax(10000)
|
||||
.enableReconnection()
|
||||
.setTimeout(20000)
|
||||
.setExtraHeaders({'Connection': 'Upgrade'})
|
||||
.build(),
|
||||
);
|
||||
_isSocketInitialized = true;
|
||||
|
||||
socket.connect();
|
||||
|
||||
socket.onConnect((_) {
|
||||
Log.print("✅ Socket Connected Successfully");
|
||||
isSocketConnected = true;
|
||||
_startHeartbeat();
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) {
|
||||
socket.emit('subscribe_driver_location', {
|
||||
'ride_id': rideLifecycle.rideId,
|
||||
'driver_id': rideLifecycle.driverId,
|
||||
});
|
||||
Log.print("📡 Re-subscribed to driver location after connect");
|
||||
}
|
||||
update();
|
||||
});
|
||||
|
||||
socket.onDisconnect((_) {
|
||||
Log.print("⚠️ Socket Disconnected — Auto-Reconnect will handle it");
|
||||
isSocketConnected = false;
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.isActiveRideState()) {
|
||||
Log.print("🔄 Enabling Fast Polling Fallback (4s) until reconnect...");
|
||||
rideLifecycle.startMasterTimerWithInterval(4);
|
||||
}
|
||||
update();
|
||||
});
|
||||
|
||||
socket.onReconnect((_) {
|
||||
Log.print("🔁 Socket Reconnected Successfully!");
|
||||
isSocketConnected = true;
|
||||
_startHeartbeat();
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.rideId != 'yet' && rideLifecycle.driverId.isNotEmpty) {
|
||||
socket.emit('subscribe_driver_location', {
|
||||
'ride_id': rideLifecycle.rideId,
|
||||
'driver_id': rideLifecycle.driverId,
|
||||
});
|
||||
Log.print("📡 Re-subscribed to driver location after reconnect");
|
||||
}
|
||||
|
||||
if (rideLifecycle.isActiveRideState()) {
|
||||
Log.print("✅ Socket back online — stopping Fast Polling Fallback");
|
||||
rideLifecycle.cancelMasterTimer();
|
||||
}
|
||||
update();
|
||||
});
|
||||
|
||||
socket.onReconnectAttempt((attemptNumber) {
|
||||
Log.print("🔄 Socket Reconnect Attempt #$attemptNumber...");
|
||||
});
|
||||
|
||||
socket.onError((error) {
|
||||
Log.print("❌ Socket Error: $error");
|
||||
isSocketConnected = false;
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) {
|
||||
Log.print("❌ Socket Connect Error: $error");
|
||||
isSocketConnected = false;
|
||||
// في الإصدار 1.0.2 أحياناً auto-reconnect لا يعمل بعد connect_error
|
||||
// نتأكد يدوياً من إعادة الاتصال
|
||||
Future.delayed(const Duration(seconds: 3), () {
|
||||
if (!isSocketConnected && _isSocketInitialized) {
|
||||
Log.print("🔄 Manual reconnect after connect_error...");
|
||||
try {
|
||||
socket.connect();
|
||||
} catch (e) {
|
||||
Log.print("Manual reconnect error: $e");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('ride_status_change', (data) {
|
||||
Log.print("📩 Socket Event: ride_status_change -> $data");
|
||||
_handleRideStatusChangeWithSocket(data);
|
||||
});
|
||||
|
||||
socket.on('driver_location_update', (data) {
|
||||
handleDriverLocationUpdate(data);
|
||||
});
|
||||
}
|
||||
|
||||
void _startHeartbeat() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: 15), (timer) {
|
||||
if (isSocketConnected && socket.connected) {
|
||||
socket.emit('heartbeat',
|
||||
{'passenger_id': box.read(BoxName.passengerID).toString()});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool isSocketHealthy() {
|
||||
if (!isSocketConnected) return false;
|
||||
if (_lastSocketLocationTime == null) return false;
|
||||
final diff = DateTime.now().difference(_lastSocketLocationTime!).inSeconds;
|
||||
return diff < 20;
|
||||
}
|
||||
|
||||
void _handleRideStatusChangeWithSocket(dynamic data) {
|
||||
if (data == null || data['status'] == null) return;
|
||||
|
||||
String newStatus = data['status'].toString().toLowerCase();
|
||||
Log.print("🔔 Socket Status Update: $newStatus");
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
|
||||
Map<String, dynamic>? driverInfo;
|
||||
if (data['driver_info'] != null && data['driver_info'] is Map) {
|
||||
driverInfo = Map<String, dynamic>.from(data['driver_info']);
|
||||
}
|
||||
|
||||
switch (newStatus) {
|
||||
case 'accepted':
|
||||
case 'apply':
|
||||
case 'applied':
|
||||
rideLifecycle.processRideAcceptance(
|
||||
driverData: driverInfo, source: "Socket");
|
||||
break;
|
||||
|
||||
case 'arrived':
|
||||
rideLifecycle.processDriverArrival("Socket");
|
||||
break;
|
||||
|
||||
case 'started':
|
||||
case 'begin':
|
||||
rideLifecycle.processRideBegin(source: "Socket");
|
||||
break;
|
||||
|
||||
case 'finished':
|
||||
case 'ended':
|
||||
_onRideFinishedWithSocket(data);
|
||||
break;
|
||||
|
||||
case 'cancelled':
|
||||
rideLifecycle.processRideCancelledByDriver(data, source: "Socket");
|
||||
break;
|
||||
|
||||
case 'no_drivers_found':
|
||||
rideLifecycle.showNoDriverDialog();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _onRideFinishedWithSocket(dynamic data) {
|
||||
Log.print("🏁 Ride Finished (Socket)");
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
|
||||
var rawList = data['DriverList'];
|
||||
List<dynamic> listToSend = [];
|
||||
|
||||
if (rawList != null) {
|
||||
if (rawList is List) {
|
||||
listToSend = rawList;
|
||||
} else if (rawList is String) {
|
||||
try {
|
||||
listToSend = jsonDecode(rawList);
|
||||
} catch (e) {
|
||||
Log.print("Error decoding DriverList: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (listToSend.isEmpty && data['price'] != null) {
|
||||
listToSend = [
|
||||
rideLifecycle.driverId,
|
||||
rideLifecycle.rideId,
|
||||
rideLifecycle.driverToken,
|
||||
data['price'].toString()
|
||||
];
|
||||
}
|
||||
|
||||
rideLifecycle.processRideFinished(listToSend, source: "Socket");
|
||||
}
|
||||
|
||||
void handleDriverLocationUpdate(dynamic data) {
|
||||
if (!isSocketConnected || data == null) return;
|
||||
_lastSocketLocationTime = DateTime.now();
|
||||
_socketLocationUpdatesCount++;
|
||||
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.driverId.isEmpty &&
|
||||
(data['driver_id'] ?? data['driverId']) != null) {
|
||||
rideLifecycle.driverId =
|
||||
(data['driver_id'] ?? data['driverId']).toString();
|
||||
}
|
||||
|
||||
if (_socketLocationUpdatesCount >= 3 &&
|
||||
rideLifecycle.locationPollingTimer != null) {
|
||||
Log.print("✅ Socket delivering locations reliably. Stopping polling.");
|
||||
rideLifecycle.stopDriverLocationPolling();
|
||||
}
|
||||
|
||||
try {
|
||||
double lat = double.tryParse(
|
||||
(data['latitude'] ?? data['lat'])?.toString() ?? '0') ??
|
||||
0;
|
||||
double lng = double.tryParse(
|
||||
(data['longitude'] ?? data['lng'])?.toString() ?? '0') ??
|
||||
0;
|
||||
double heading = double.tryParse(data['heading']?.toString() ?? '0') ?? 0;
|
||||
|
||||
if (lat == 0 || lng == 0) return;
|
||||
|
||||
LatLng newPos = LatLng(lat, lng);
|
||||
|
||||
final nearbyDrivers = Get.find<NearbyDriversController>();
|
||||
if (nearbyDrivers.driverCarsLocationToPassengerAfterApplied.isEmpty) {
|
||||
nearbyDrivers.driverCarsLocationToPassengerAfterApplied.add(newPos);
|
||||
} else {
|
||||
nearbyDrivers.driverCarsLocationToPassengerAfterApplied[0] = newPos;
|
||||
}
|
||||
|
||||
double speed = double.tryParse(data['speed']?.toString() ?? '0') ?? 0;
|
||||
rideLifecycle.checkAndRecalculateIfDeviated(
|
||||
newPos,
|
||||
heading: heading,
|
||||
speed: speed,
|
||||
);
|
||||
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
if (mapEngine.mapController != null) {
|
||||
double zoom = 16.5;
|
||||
if (speed > 0) {
|
||||
zoom = 17.0 - ((speed - 10) / 70) * 2.5;
|
||||
zoom = zoom.clamp(14.5, 17.0);
|
||||
}
|
||||
mapEngine.mapController!
|
||||
.animateCamera(CameraUpdate.newLatLngZoom(newPos, zoom));
|
||||
}
|
||||
|
||||
final dynamic distanceValue =
|
||||
data['distance_m'] ?? data['distance_meters'] ?? data['distance'];
|
||||
final double? distanceMeters =
|
||||
double.tryParse(distanceValue?.toString() ?? '');
|
||||
final int? etaSeconds = data['eta_seconds'] == null
|
||||
? null
|
||||
: int.tryParse(data['eta_seconds'].toString());
|
||||
final bool hasServerMetrics = (etaSeconds != null && etaSeconds > 0) ||
|
||||
(distanceMeters != null && distanceMeters > 0);
|
||||
if (hasServerMetrics) {
|
||||
rideLifecycle.updateDriverRouteMetrics(
|
||||
etaSeconds: etaSeconds != null && etaSeconds > 0 ? etaSeconds : null,
|
||||
distanceMeters: distanceMeters,
|
||||
);
|
||||
}
|
||||
|
||||
rideLifecycle.updateDriverMarker(newPos, heading);
|
||||
rideLifecycle.updateRemainingRoute(newPos, updateEta: !hasServerMetrics);
|
||||
rideLifecycle.update();
|
||||
} catch (e) {
|
||||
Log.print('Error in handleDriverLocationUpdate: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void disposeRideSocket() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_watchdogTimer?.cancel();
|
||||
if (_isSocketInitialized) {
|
||||
socket.disconnect();
|
||||
socket.dispose();
|
||||
isSocketConnected = false;
|
||||
_isSocketInitialized = false;
|
||||
Log.print("🔌 Socket Disposed");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
disposeRideSocket();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
475
lib/controller/home/map/nearby_drivers_controller.dart
Normal file
475
lib/controller/home/map/nearby_drivers_controller.dart
Normal file
@@ -0,0 +1,475 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' show Random, atan2, cos, pi, pow, sin, sqrt;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/api_key.dart';
|
||||
import '../../../print.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import 'map_engine_controller.dart';
|
||||
import 'location_search_controller.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import '../../../models/model/locations.dart';
|
||||
import 'car_location.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
|
||||
class NearbyDriversController extends GetxController {
|
||||
List carsLocationByPassenger = [];
|
||||
List<LatLng> driverCarsLocationToPassengerAfterApplied = [];
|
||||
List<CarLocationModel> carLocationsModels = [];
|
||||
String? currentDriverMarkerId;
|
||||
bool lowPerf = false;
|
||||
|
||||
dynamic dataCarsLocationByPassenger;
|
||||
bool noCarString = false;
|
||||
final double minMovementThreshold = 2.0;
|
||||
final Map<String, Timer> _animationTimers = {};
|
||||
|
||||
final List<Map<String, dynamic>> fakeCarData = [];
|
||||
|
||||
Future<bool> getCarsLocationByPassengerAndReloadMarker() async {
|
||||
carsLocationByPassenger = [];
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (locSearch.passengerLocation.latitude == 0 && locSearch.passengerLocation.longitude == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getCarsLocationByPassenger,
|
||||
payload: {
|
||||
'lat': locSearch.passengerLocation.latitude.toString(),
|
||||
'lng': locSearch.passengerLocation.longitude.toString(),
|
||||
'radius': '5',
|
||||
'limit': '50',
|
||||
},
|
||||
);
|
||||
|
||||
if (res == 'failure') {
|
||||
noCarString = true;
|
||||
dataCarsLocationByPassenger = 'failure';
|
||||
update();
|
||||
return false;
|
||||
}
|
||||
|
||||
noCarString = false;
|
||||
var responseData = jsonDecode(res);
|
||||
dataCarsLocationByPassenger = responseData;
|
||||
|
||||
List driversList = [];
|
||||
if (responseData['status'] == true && responseData['data'] != null) {
|
||||
driversList = responseData['data'];
|
||||
} else if (responseData['message'] != null) {
|
||||
driversList = responseData['message'];
|
||||
}
|
||||
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
|
||||
if (driversList.isEmpty) {
|
||||
carsLocationByPassenger.clear();
|
||||
mapEngine.update();
|
||||
return false;
|
||||
}
|
||||
|
||||
carsLocationByPassenger.clear();
|
||||
|
||||
for (var i = 0; i < driversList.length; i++) {
|
||||
var carData = driversList[i];
|
||||
|
||||
double lat = double.tryParse(carData['latitude'].toString()) ?? 0.0;
|
||||
double lng = double.tryParse(carData['longitude'].toString()) ?? 0.0;
|
||||
double heading = double.tryParse(carData['heading'].toString()) ?? 0.0;
|
||||
|
||||
if (lat == 0.0 || lng == 0.0) continue;
|
||||
|
||||
String driverId = (carData['driver_id'] ?? carData['id'] ?? '').toString();
|
||||
if (driverId.isEmpty || driverId == 'null') continue;
|
||||
|
||||
_updateOrCreateMarker(
|
||||
driverId,
|
||||
LatLng(lat, lng),
|
||||
heading,
|
||||
_getIconForCar(carData),
|
||||
);
|
||||
}
|
||||
|
||||
mapEngine.update();
|
||||
return true;
|
||||
}
|
||||
|
||||
void _addFakeCarMarkers(LatLng center, int count) {
|
||||
if (fakeCarData.isEmpty) {
|
||||
Random random = Random();
|
||||
double radiusInKm = 2.5;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
double angle = random.nextDouble() * 2 * pi;
|
||||
double distance = sqrt(random.nextDouble()) * radiusInKm;
|
||||
|
||||
double latOffset = (distance / 111.32);
|
||||
double lonOffset =
|
||||
(distance / (111.32 * cos(center.latitude * pi / 180.0)));
|
||||
|
||||
double lat = center.latitude + (latOffset * cos(angle));
|
||||
double lon = center.longitude + (lonOffset * sin(angle));
|
||||
|
||||
double heading = random.nextDouble() * 360;
|
||||
|
||||
fakeCarData.add({
|
||||
'id': 'fake_$i',
|
||||
'latitude': lat,
|
||||
'longitude': lon,
|
||||
'heading': heading,
|
||||
'gender': 'Male',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (var carData in fakeCarData) {
|
||||
_updateOrCreateMarker(
|
||||
carData['id'].toString(),
|
||||
LatLng(carData['latitude'], carData['longitude']),
|
||||
carData['heading'],
|
||||
_getIconForCar(carData),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void addFakeCarMarkers(LatLng center, int count) {
|
||||
_addFakeCarMarkers(center, count);
|
||||
}
|
||||
|
||||
Future<CarLocation?> getNearestDriverByPassengerLocation() async {
|
||||
final rideLife = Get.find<RideLifecycleController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (!rideLife.rideConfirm) {
|
||||
if (dataCarsLocationByPassenger != 'failure' &&
|
||||
dataCarsLocationByPassenger != null &&
|
||||
dataCarsLocationByPassenger.containsKey('message') &&
|
||||
dataCarsLocationByPassenger['message'] != null &&
|
||||
dataCarsLocationByPassenger['message'].length > 0) {
|
||||
double nearestDistance = double.infinity;
|
||||
CarLocation? nearestCar;
|
||||
|
||||
for (var i = 0;
|
||||
i < dataCarsLocationByPassenger['message'].length;
|
||||
i++) {
|
||||
var carLocation = dataCarsLocationByPassenger['message'][i];
|
||||
|
||||
try {
|
||||
final distance = Geolocator.distanceBetween(
|
||||
locSearch.passengerLocation.latitude,
|
||||
locSearch.passengerLocation.longitude,
|
||||
double.parse(carLocation['latitude']),
|
||||
double.parse(carLocation['longitude']),
|
||||
);
|
||||
|
||||
int durationToPassenger = (distance / 1000 / 25 * 3600).round();
|
||||
update();
|
||||
|
||||
if (distance < nearestDistance) {
|
||||
nearestDistance = distance;
|
||||
|
||||
nearestCar = CarLocation(
|
||||
distance: distance,
|
||||
duration: durationToPassenger.toDouble(),
|
||||
id: carLocation['driver_id'],
|
||||
latitude: double.parse(carLocation['latitude']),
|
||||
longitude: double.parse(carLocation['longitude']),
|
||||
);
|
||||
update();
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('Error calculating distance/duration: $e');
|
||||
}
|
||||
}
|
||||
return nearestCar;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<CarLocation?> getNearestDriverByPassengerLocationAPIGOOGLE() async {
|
||||
final rideLife = Get.find<RideLifecycleController>();
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
if (mapEngine.polyLines.isEmpty || rideLife.totalCostPassenger == 0) {
|
||||
return null;
|
||||
}
|
||||
if (!rideLife.rideConfirm) {
|
||||
double nearestDistance = double.infinity;
|
||||
if (dataCarsLocationByPassenger != 'failure' &&
|
||||
dataCarsLocationByPassenger != null &&
|
||||
dataCarsLocationByPassenger.containsKey('message') &&
|
||||
dataCarsLocationByPassenger['message'] != null) {
|
||||
if (dataCarsLocationByPassenger['message'].length > 0) {
|
||||
CarLocation? nearestCar;
|
||||
for (var i = 0;
|
||||
i < dataCarsLocationByPassenger['message'].length;
|
||||
i++) {
|
||||
var carLocation = dataCarsLocationByPassenger['message'][i];
|
||||
|
||||
update();
|
||||
String apiUrl =
|
||||
'${AppLink.googleMapsLink}distancematrix/json?destinations=${carLocation['latitude']},${carLocation['longitude']}&origins=${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}&units=metric&key=${AK.mapAPIKEY}';
|
||||
var response = await CRUD().getGoogleApi(link: apiUrl, payload: {});
|
||||
if (response != null && response['status'] == "OK") {
|
||||
var data = response;
|
||||
int distance1 =
|
||||
data['rows'][0]['elements'][0]['distance']['value'];
|
||||
rideLife.distanceByPassenger =
|
||||
data['rows'][0]['elements'][0]['distance']['text'];
|
||||
rideLife.durationToPassenger =
|
||||
data['rows'][0]['elements'][0]['duration']['value'];
|
||||
|
||||
Duration durationFromDriverToPassenger =
|
||||
Duration(seconds: rideLife.durationToPassenger.toInt());
|
||||
rideLife.stringRemainingTimeToPassenger =
|
||||
data['rows'][0]['elements'][0]['duration']['text'];
|
||||
update();
|
||||
if (distance1 < nearestDistance) {
|
||||
nearestDistance = distance1.toDouble();
|
||||
|
||||
nearestCar = CarLocation(
|
||||
distance: distance1.toDouble(),
|
||||
duration: rideLife.durationToPassenger.toDouble(),
|
||||
id: carLocation['driver_id'],
|
||||
latitude: double.parse(carLocation['latitude']),
|
||||
longitude: double.parse(carLocation['longitude']),
|
||||
);
|
||||
update();
|
||||
}
|
||||
} else {
|
||||
Log.print('${response?['status']}: error Google distance matrix');
|
||||
}
|
||||
}
|
||||
return nearestCar;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future getCarForFirstConfirm(String carType) async {
|
||||
bool foundCars = false;
|
||||
int attempt = 0;
|
||||
|
||||
Timer.periodic(const Duration(seconds: 4), (Timer t) async {
|
||||
foundCars = await getCarsLocationByPassengerAndReloadMarker();
|
||||
Log.print('foundCars: $foundCars');
|
||||
|
||||
if (foundCars) {
|
||||
t.cancel();
|
||||
} else if (attempt >= 4) {
|
||||
t.cancel();
|
||||
if (!foundCars) {
|
||||
noCarString = true;
|
||||
dataCarsLocationByPassenger = 'failure';
|
||||
}
|
||||
update();
|
||||
}
|
||||
attempt++;
|
||||
});
|
||||
}
|
||||
|
||||
void startCarLocationSearch(String carType) {
|
||||
int searchInterval = 5;
|
||||
Log.print('searchInterval: $searchInterval');
|
||||
int boundIncreaseStep = 2500;
|
||||
Log.print('boundIncreaseStep: $boundIncreaseStep');
|
||||
int maxAttempts = 3;
|
||||
int maxBoundIncreaseStep = 6000;
|
||||
int attempt = 0;
|
||||
Log.print('initial attempt: $attempt');
|
||||
|
||||
Timer.periodic(Duration(seconds: searchInterval), (Timer timer) async {
|
||||
Log.print('Current attempt: $attempt');
|
||||
bool foundCars = false;
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
if (attempt >= maxAttempts) {
|
||||
timer.cancel();
|
||||
if (foundCars == false) {
|
||||
noCarString = true;
|
||||
update();
|
||||
}
|
||||
} else if (mapEngine.reloadStartApp == true) {
|
||||
Log.print('reloadStartApp: ${mapEngine.reloadStartApp}');
|
||||
foundCars = await getCarsLocationByPassengerAndReloadMarker();
|
||||
Log.print('foundCars: $foundCars');
|
||||
|
||||
if (foundCars) {
|
||||
timer.cancel();
|
||||
} else {
|
||||
attempt++;
|
||||
Log.print('Incrementing attempt to: $attempt');
|
||||
|
||||
if (boundIncreaseStep < maxBoundIncreaseStep) {
|
||||
boundIncreaseStep += 1500;
|
||||
if (boundIncreaseStep > maxBoundIncreaseStep) {
|
||||
boundIncreaseStep = maxBoundIncreaseStep;
|
||||
}
|
||||
Log.print('New boundIncreaseStep: $boundIncreaseStep');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
String _getIconForCar(Map<String, dynamic> carData) {
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
if (carData['model'].toString().contains('دراجة')) {
|
||||
return mapEngine.motoIcon;
|
||||
} else if (carData['gender'] == 'Female') {
|
||||
return mapEngine.ladyIcon;
|
||||
} else {
|
||||
return mapEngine.carIcon;
|
||||
}
|
||||
}
|
||||
|
||||
void _updateOrCreateMarker(
|
||||
String markerId, LatLng newPosition, double newHeading, String icon) {
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
if (!mapEngine.isIconsLoaded) {
|
||||
Log.print("⚠️ Skipping drawing marker $markerId because map icons are not fully loaded yet.");
|
||||
return;
|
||||
}
|
||||
final mId = MarkerId(markerId);
|
||||
final existingMarker = mapEngine.markers.cast<Marker?>().firstWhere(
|
||||
(m) => m?.markerId == mId,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (existingMarker == null) {
|
||||
mapEngine.markers = {
|
||||
...mapEngine.markers,
|
||||
Marker(
|
||||
markerId: mId,
|
||||
position: newPosition,
|
||||
rotation: newHeading,
|
||||
icon: InlqBitmap.fromStyleImage(icon),
|
||||
anchor: const Offset(0.5, 0.5),
|
||||
),
|
||||
};
|
||||
mapEngine.update();
|
||||
} else {
|
||||
double distance = Geolocator.distanceBetween(
|
||||
existingMarker.position.latitude,
|
||||
existingMarker.position.longitude,
|
||||
newPosition.latitude,
|
||||
newPosition.longitude);
|
||||
if (distance >= minMovementThreshold) {
|
||||
_smoothlyUpdateMarker(existingMarker, newPosition, newHeading, icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _smoothlyUpdateMarker(
|
||||
Marker oldMarker, LatLng newPosition, double newHeading, String icon) {
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
final MarkerId markerIdKey = oldMarker.markerId;
|
||||
|
||||
_animationTimers[markerIdKey.value]?.cancel();
|
||||
|
||||
int ticks = 0;
|
||||
const int totalSteps = 20;
|
||||
const int stepDuration = 50;
|
||||
|
||||
double latStep =
|
||||
(newPosition.latitude - oldMarker.position.latitude) / totalSteps;
|
||||
double lngStep =
|
||||
(newPosition.longitude - oldMarker.position.longitude) / totalSteps;
|
||||
double headingStep = (newHeading - oldMarker.rotation) / totalSteps;
|
||||
|
||||
LatLng currentPos = oldMarker.position;
|
||||
double currentHeading = oldMarker.rotation;
|
||||
|
||||
_animationTimers[markerIdKey.value] =
|
||||
Timer.periodic(const Duration(milliseconds: stepDuration), (timer) {
|
||||
ticks++;
|
||||
|
||||
currentPos =
|
||||
LatLng(currentPos.latitude + latStep, currentPos.longitude + lngStep);
|
||||
currentHeading += headingStep;
|
||||
|
||||
final updatedMarker = oldMarker.copyWith(
|
||||
position: currentPos,
|
||||
rotation: currentHeading,
|
||||
icon: InlqBitmap.fromStyleImage(icon),
|
||||
);
|
||||
|
||||
mapEngine.markers = {
|
||||
...mapEngine.markers.where((m) => m.markerId != markerIdKey),
|
||||
updatedMarker,
|
||||
};
|
||||
|
||||
if (mapEngine.mapController != null) {
|
||||
mapEngine.mapController!.animateCamera(CameraUpdate.newLatLng(currentPos));
|
||||
}
|
||||
|
||||
mapEngine.update();
|
||||
|
||||
if (ticks >= totalSteps) {
|
||||
timer.cancel();
|
||||
_animationTimers.remove(markerIdKey.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
double calculateBearing(double lat1, double lon1, double lat2, double lon2) {
|
||||
double deltaLon = lon2 - lon1;
|
||||
double y = sin(deltaLon) * cos(lat2);
|
||||
double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(deltaLon);
|
||||
double bearing = atan2(y, x);
|
||||
return (bearing * 180 / pi + 360) % 360;
|
||||
}
|
||||
|
||||
void analyzeBehavior(Position currentPosition, List<LatLng> routePoints) {
|
||||
double actualBearing = currentPosition.heading;
|
||||
double expectedBearing = calculateBearing(
|
||||
routePoints[0].latitude,
|
||||
routePoints[0].longitude,
|
||||
routePoints[1].latitude,
|
||||
routePoints[1].longitude,
|
||||
);
|
||||
|
||||
double bearingDifference = (expectedBearing - actualBearing).abs();
|
||||
if (bearingDifference > 30) {
|
||||
Log.print("⚠️ السائق انحرف عن المسار!");
|
||||
}
|
||||
}
|
||||
|
||||
void detectStops(Position currentPosition) {
|
||||
if (currentPosition.speed < 0.5) {
|
||||
Log.print("🚦 السائق توقف في موقع غير متوقع!");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> detectPerfMode() async {
|
||||
try {
|
||||
if (GetPlatform.isAndroid) {
|
||||
final info = await DeviceInfoPlugin().androidInfo;
|
||||
final sdk = info.version.sdkInt;
|
||||
final ram = info.availableRamSize;
|
||||
lowPerf = (sdk < 28) || (ram > 0 && ram < 3 * 1024 * 1024 * 1024);
|
||||
} else {
|
||||
lowPerf = false;
|
||||
}
|
||||
} catch (_) {
|
||||
lowPerf = false;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_animationTimers.forEach((key, timer) => timer.cancel());
|
||||
_animationTimers.clear();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
4558
lib/controller/home/map/ride_lifecycle_controller.dart
Normal file
4558
lib/controller/home/map/ride_lifecycle_controller.dart
Normal file
File diff suppressed because it is too large
Load Diff
10
lib/controller/home/map/ride_state.dart
Normal file
10
lib/controller/home/map/ride_state.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
enum RideState {
|
||||
noRide, // لا يوجد رحلة جارية، عرض واجهة البحث
|
||||
cancelled, // تم إلغاء الرحلة
|
||||
preCheckReview, // يوجد رحلة منتهية، تحقق من التقييم
|
||||
searching, // جاري البحث عن كابتن
|
||||
driverApplied, // تم قبول الطلب
|
||||
driverArrived, // وصل السائق
|
||||
inProgress, // الرحلة بدأت بالفعل
|
||||
finished, // انتهت الرحلة (سيتم تحويلها إلى preCheckReview)
|
||||
}
|
||||
436
lib/controller/home/map/ui_interactions_controller.dart
Normal file
436
lib/controller/home/map/ui_interactions_controller.dart
Normal file
@@ -0,0 +1,436 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../main.dart'; // contains global 'box'
|
||||
import '../../../print.dart';
|
||||
import '../../../services/emergency_signal_service.dart';
|
||||
import '../../../views/widgets/elevated_btn.dart';
|
||||
import '../../../views/widgets/mydialoug.dart';
|
||||
import '../../../views/widgets/my_textField.dart';
|
||||
import '../../../views/home/map_page_passenger.dart';
|
||||
import '../../../views/widgets/error_snakbar.dart';
|
||||
import '../../../models/model/painter_copoun.dart';
|
||||
import '../../functions/launch.dart';
|
||||
import '../../firebase/local_notification.dart';
|
||||
import '../../firebase/notification_service.dart';
|
||||
import '../../functions/crud.dart';
|
||||
import '../../functions/tts.dart';
|
||||
import 'ride_lifecycle_controller.dart';
|
||||
import 'location_search_controller.dart';
|
||||
import 'map_engine_controller.dart';
|
||||
|
||||
class UiInteractionsController extends GetxController {
|
||||
TextEditingController sosPhonePassengerProfile = TextEditingController();
|
||||
TextEditingController whatsAppLocationText = TextEditingController();
|
||||
final sosFormKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
EmergencySignalService.instance.startListening(() {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (rideLifecycle.statusRide == 'Begin' ||
|
||||
rideLifecycle.statusRide == 'start') {
|
||||
Log.print("🚨 Emergency shake verified! Prompting SOS...");
|
||||
sosPassenger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _ensureSosNumber(Function onSuccess) async {
|
||||
String? storedPhone = box.read(BoxName.sosPhonePassenger);
|
||||
if (storedPhone != null && storedPhone.isNotEmpty) {
|
||||
onSuccess();
|
||||
return;
|
||||
}
|
||||
|
||||
sosPhonePassengerProfile.clear();
|
||||
Get.defaultDialog(
|
||||
title: 'Add SOS Phone'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Form(
|
||||
key: sosFormKey,
|
||||
child: Column(
|
||||
children: [
|
||||
MyTextForm(
|
||||
controller: sosPhonePassengerProfile,
|
||||
label: 'insert sos phone'.tr,
|
||||
hint: 'e.g. 0912345678 (Default +963)'.tr,
|
||||
type: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Note: If no country code is entered, it will be saved as Syrian (+963).".tr,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Save'.tr,
|
||||
onPressed: () async {
|
||||
if (sosFormKey.currentState!.validate()) {
|
||||
Get.back();
|
||||
var numberPhone =
|
||||
formatSyrianPhoneNumber(sosPhonePassengerProfile.text);
|
||||
|
||||
await CRUD().post(
|
||||
link: AppLink.updateprofile,
|
||||
payload: {
|
||||
'id': box.read(BoxName.passengerID),
|
||||
'sosPhone': numberPhone,
|
||||
},
|
||||
);
|
||||
|
||||
box.write(BoxName.sosPhonePassenger, numberPhone);
|
||||
onSuccess();
|
||||
}
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Cancel'.tr,
|
||||
onPressed: () => Get.back(),
|
||||
kolor: AppColor.redColor,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
void sosPassenger() {
|
||||
_ensureSosNumber(() {
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: "Emergency SOS".tr,
|
||||
titleStyle: AppStyle.title.copyWith(color: AppColor.redColor),
|
||||
content: Column(
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, size: 50, color: AppColor.redColor),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Do you want to send an emergency message to your SOS contact?".tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: "Send SOS".tr,
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
_shareTripDetailsSOS();
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: "I'm Safe".tr,
|
||||
kolor: AppColor.greenColor,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _shareTripDetailsSOS() {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
|
||||
String message = "**Emergency SOS from Passenger:**\n";
|
||||
String origin = locSearch.startNameAddress;
|
||||
String destination = locSearch.endNameAddress;
|
||||
|
||||
message += "* ${'Origin'.tr}: $origin\n";
|
||||
message += "* ${'Destination'.tr}: $destination\n";
|
||||
message += "* ${'Driver Name'.tr}: ${rideLifecycle.driverName}\n";
|
||||
message +=
|
||||
"* ${'Car'.tr}: ${rideLifecycle.make} - ${rideLifecycle.model} - ${rideLifecycle.licensePlate}\n";
|
||||
message += "* ${'Phone'.tr}: ${rideLifecycle.driverPhone}\n\n";
|
||||
|
||||
message +=
|
||||
"${'Location'.tr}: https://www.google.com/maps/search/?api=1&query=${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}\n";
|
||||
message += "Please help! Contact me as soon as possible.".tr;
|
||||
|
||||
launchCommunication(
|
||||
'whatsapp', box.read(BoxName.sosPhonePassenger), message);
|
||||
}
|
||||
|
||||
String formatSyrianPhone(String phone) {
|
||||
phone = phone.replaceAll(' ', '').replaceAll('+', '');
|
||||
if (phone.startsWith('00963')) {
|
||||
phone = phone.replaceFirst('00963', '963');
|
||||
}
|
||||
if (phone.startsWith('0963')) {
|
||||
phone = phone.replaceFirst('0963', '963');
|
||||
}
|
||||
if (phone.startsWith('963')) {
|
||||
return phone;
|
||||
}
|
||||
if (phone.startsWith('09')) {
|
||||
return '963' + phone.substring(1);
|
||||
}
|
||||
if (phone.startsWith('9') && phone.length == 9) {
|
||||
return '963' + phone;
|
||||
}
|
||||
return phone;
|
||||
}
|
||||
|
||||
String formatSyrianPhoneNumber(String phoneNumber) {
|
||||
String trimmedPhone = phoneNumber.trim();
|
||||
if (trimmedPhone.startsWith('09')) {
|
||||
return '963${trimmedPhone.substring(1)}';
|
||||
}
|
||||
if (trimmedPhone.startsWith('963')) {
|
||||
return trimmedPhone;
|
||||
}
|
||||
return '963$trimmedPhone';
|
||||
}
|
||||
|
||||
void sendSMS(String to) async {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
String formattedDriverPhone =
|
||||
rideLifecycle.driverPhone.replaceAll(' ', '').replaceAll('+', '');
|
||||
|
||||
String message =
|
||||
'Hi! This is ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n I am using ${box.read(AppInformation.appName)} to ride with ${rideLifecycle.passengerName} as the driver. ${rideLifecycle.passengerName} \nis driving a ${rideLifecycle.model}\n with license plate ${rideLifecycle.licensePlate}.\n I am currently located at ${Get.find<LocationSearchController>().passengerLocation}.\n If you need to reach me, please contact the driver directly at\n\n $formattedDriverPhone.';
|
||||
|
||||
launchCommunication('sms', to, message);
|
||||
}
|
||||
|
||||
void sendWhatsapp(String to) async {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
final locSearch = Get.find<LocationSearchController>();
|
||||
String formattedPhone = formatSyrianPhone(to);
|
||||
|
||||
String message =
|
||||
'${'${'Hi! This is'.tr} ${(box.read(BoxName.name).toString().split(' ')[0]).toString()}.\n${' I am using'.tr}'} ${AppInformation.appName}${' to ride with'.tr} ${rideLifecycle.passengerName}${' as the driver.'.tr} ${rideLifecycle.passengerName} \n${'is driving a '.tr}${rideLifecycle.model}\n${' with license plate '.tr}${rideLifecycle.licensePlate}.\n${' I am currently located at '.tr} https://www.google.com/maps/place/${locSearch.passengerLocation.latitude},${locSearch.passengerLocation.longitude}.\n${' If you need to reach me, please contact the driver directly at'.tr}\n\n ${rideLifecycle.driverPhone}.';
|
||||
|
||||
launchCommunication('whatsapp', formattedPhone, message);
|
||||
}
|
||||
|
||||
Future<dynamic> driverArrivePassengerDialoge() {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
return Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: 'Hi ,I Arrive your location'.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
middleText: 'Please go to Car Driver'.tr,
|
||||
middleTextStyle: AppStyle.title,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok I will go now.'.tr,
|
||||
onPressed: () {
|
||||
NotificationService.sendNotification(
|
||||
target: rideLifecycle.driverToken.toString(),
|
||||
title: 'Hi ,I will go now'.tr,
|
||||
body: 'I will go now'.tr,
|
||||
isTopic: false,
|
||||
tone: 'ding',
|
||||
driverList: [],
|
||||
category: 'Hi ,I will go now',
|
||||
);
|
||||
Get.back();
|
||||
rideLifecycle.remainingTime = 0;
|
||||
rideLifecycle.update();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void getDialog(String title, String? midTitle, VoidCallback onPressed) {
|
||||
final textToSpeechController = Get.find<TextToSpeechController>();
|
||||
Get.defaultDialog(
|
||||
title: title,
|
||||
titleStyle: AppStyle.title,
|
||||
middleTextStyle: AppStyle.title,
|
||||
content: Column(
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await textToSpeechController.speakText(title ?? midTitle!);
|
||||
},
|
||||
icon: const Icon(Icons.headphones),
|
||||
),
|
||||
Text(
|
||||
midTitle!,
|
||||
style: AppStyle.title,
|
||||
)
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
onPressed: onPressed,
|
||||
kolor: AppColor.greenColor,
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Cancel',
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future shareTripWithFamily() async {
|
||||
_ensureSosNumber(() {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
String storedPhone = box.read(BoxName.sosPhonePassenger)!;
|
||||
|
||||
if (rideLifecycle.rideId == 'yet' || rideLifecycle.driverId.isEmpty) {
|
||||
Get.snackbar("Alert".tr, "Wait for the trip to start first".tr);
|
||||
return;
|
||||
}
|
||||
|
||||
var numberPhone = formatSyrianPhoneNumber(storedPhone);
|
||||
String trackingLink = rideLifecycle.generateTrackingLink(
|
||||
rideLifecycle.rideId, rideLifecycle.driverId);
|
||||
|
||||
String message = """
|
||||
مرحباً، تابع رحلتي مباشرة على تطبيق انطلق 🚗
|
||||
|
||||
يمكنك تتبع مسار الرحلة من هنا:
|
||||
$trackingLink
|
||||
|
||||
السائق: ${rideLifecycle.passengerName}
|
||||
السيارة: ${rideLifecycle.model} - ${rideLifecycle.licensePlate}
|
||||
شكراً لاستخدامك انطلق!
|
||||
"""
|
||||
.tr;
|
||||
|
||||
String messageEn = """Hello, follow my trip live on Intaleq 🚗
|
||||
|
||||
Track my ride here:
|
||||
$trackingLink
|
||||
|
||||
Driver: ${rideLifecycle.passengerName}
|
||||
Car: ${rideLifecycle.model} - ${rideLifecycle.licensePlate}
|
||||
Thank you for using Intaleq!
|
||||
""";
|
||||
|
||||
String userLanguage = box.read(BoxName.lang) ?? 'ar';
|
||||
message = (userLanguage == 'ar') ? message : messageEn;
|
||||
|
||||
Log.print("Sending WhatsApp to: $numberPhone");
|
||||
launchCommunication('whatsapp', numberPhone, message);
|
||||
|
||||
box.write(BoxName.parentTripSelected, true);
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
Future getTokenForParent() async {
|
||||
_ensureSosNumber(() async {
|
||||
String storedPhone = box.read(BoxName.sosPhonePassenger)!;
|
||||
var numberPhone = formatSyrianPhoneNumber(storedPhone);
|
||||
Log.print("Searching for Parent Token with Phone: $numberPhone");
|
||||
|
||||
var res = await CRUD()
|
||||
.post(link: AppLink.getTokenParent, payload: {'phone': numberPhone});
|
||||
|
||||
if (res is Map<String, dynamic>) {
|
||||
handleResponse(res);
|
||||
} else {
|
||||
try {
|
||||
var decoded = jsonDecode(res);
|
||||
handleResponse(decoded);
|
||||
} catch (e) {
|
||||
Log.print("Error parsing parent response: $res");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void handleResponse(Map<String, dynamic> res) {
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
if (res['status'] == 'failure') {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
|
||||
Get.defaultDialog(
|
||||
title: "No user found".tr,
|
||||
titleStyle: AppStyle.title,
|
||||
content: Column(
|
||||
children: [
|
||||
Text(
|
||||
"No passenger found for the given phone number".tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
"Send Intaleq app to him".tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(color: AppColor.greenColor, fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Send Invite'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
var rawPhone = box.read(BoxName.sosPhonePassenger);
|
||||
if (rawPhone == null) return;
|
||||
var phone = formatSyrianPhoneNumber(rawPhone);
|
||||
|
||||
var message = '''Dear Friend,
|
||||
|
||||
🚀 I have just started an exciting trip on Intaleq!
|
||||
Download the app to track my ride:
|
||||
|
||||
👉 Android: https://play.google.com/store/apps/details?id=com.Intaleq.intaleq&hl=en-US
|
||||
👉 iOS: https://apps.apple.com/st/app/intaleq-rider/id6748075179
|
||||
|
||||
See you there!
|
||||
Intaleq Team''';
|
||||
|
||||
launchCommunication('whatsapp', phone, message);
|
||||
},
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'Cancel'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
);
|
||||
} else if (res['status'] == 'success') {
|
||||
if (Get.isDialogOpen ?? false) Get.back();
|
||||
|
||||
Get.snackbar("Success".tr, "The invitation was sent successfully".tr,
|
||||
backgroundColor: AppColor.greenColor, colorText: Colors.white);
|
||||
|
||||
List tokensData = res['data'];
|
||||
for (var device in tokensData) {
|
||||
String tokenParent = device['token'];
|
||||
|
||||
NotificationService.sendNotification(
|
||||
category: "Trip Monitoring",
|
||||
target: tokenParent,
|
||||
title: "Trip Monitoring".tr,
|
||||
body: "Click to track the trip".tr,
|
||||
isTopic: false,
|
||||
tone: 'tone1',
|
||||
driverList: [rideLifecycle.rideId, rideLifecycle.driverId],
|
||||
);
|
||||
box.write(BoxName.tokenParent, tokenParent);
|
||||
}
|
||||
box.write(BoxName.parentTripSelected, true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
EmergencySignalService.instance.stopListening();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:Intaleq/constant/style.dart';
|
||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||
|
||||
import '../../constant/api_key.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../functions/crud.dart';
|
||||
import '../functions/location_controller.dart';
|
||||
|
||||
@@ -114,14 +114,21 @@ class WayPointController extends GetxController {
|
||||
void onInit() {
|
||||
// Get.put(LocationController());
|
||||
addWayPoints();
|
||||
myLocation = Get.find<MapPassengerController>().passengerLocation;
|
||||
myLocation = Get.find<RideLifecycleController>().passengerLocation;
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
wayPoints.clear();
|
||||
addWayPoints();
|
||||
placeListResponse.clear();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
class PlaceList extends StatelessWidget {
|
||||
// Get the controller instance
|
||||
final controller = Get.put(WayPointController());
|
||||
final controller = Get.find<WayPointController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
159
lib/controller/home/precise_comparison_results.txt
Normal file
159
lib/controller/home/precise_comparison_results.txt
Normal file
@@ -0,0 +1,159 @@
|
||||
--- PRECISE MISSING METHODS ---
|
||||
Total original methods/getters/setters: 270
|
||||
Total defined in split controllers: 270
|
||||
Total missing: 53
|
||||
- Column
|
||||
- CupertinoDialogAction
|
||||
- Future
|
||||
- _applyLowEndModeIfNeeded
|
||||
- _buildOsrmWaypointCoords
|
||||
- _calculateDistance
|
||||
- _checkAndRecalculateIfDeviated
|
||||
- _fillDriverDataLocally
|
||||
- _haversineKm
|
||||
- _initMinimalIcons
|
||||
- _initializePolygons
|
||||
- _isActiveRideState
|
||||
- _kmToLatDelta
|
||||
- _kmToLngDelta
|
||||
- _onDriverAcceptedWithSocket
|
||||
- _onDriverArrivedWithSocket
|
||||
- _onRideCancelledWithSocket
|
||||
- _onRideStartedWithSocket
|
||||
- _playRouteAnimation
|
||||
- _relevanceScore
|
||||
- _restorePolyline
|
||||
- _retryProcess
|
||||
- _stageNiceToHave
|
||||
- _stagePricingAndState
|
||||
- _startMasterTimer
|
||||
- _startMasterTimerWithInterval
|
||||
- _startPollingFallback
|
||||
- _stopDriverLocationPolling
|
||||
- _updateDriverMarker
|
||||
- addPostFrameCallback
|
||||
- cancelRide
|
||||
- checkPassengerLocation
|
||||
- currentDriverMarkerId
|
||||
- detectPerfMode
|
||||
- directions
|
||||
- getAIKey
|
||||
- getDirectionMap
|
||||
- getDistanceFromDriverAfterAcceptedRide
|
||||
- getMapPointsForAllMethods
|
||||
- getPassengerLocationUniversity
|
||||
- getRideStatus
|
||||
- handleActiveRideOnStartup
|
||||
- handleNoDriverFound
|
||||
- isDriversDataValid
|
||||
- lastWhere
|
||||
- onChangedPassengerCount
|
||||
- onChangedPassengersChoose
|
||||
- processRideAcceptance
|
||||
- retrySearchForDrivers
|
||||
- showDrawingBottomSheet
|
||||
- showNoDriversDialog
|
||||
- startSearchingTimer
|
||||
- wait
|
||||
|
||||
--- PRECISE MISSING VARIABLES/FIELDS ---
|
||||
Total original variables: 626
|
||||
Total defined in split controllers: 558
|
||||
Total missing: 97
|
||||
- EdgeInsets
|
||||
- Error
|
||||
- InfoWindow
|
||||
- LatLngBounds
|
||||
- LocationData
|
||||
- R
|
||||
- _buildOsrmWaypointCoords
|
||||
- _calculateDistance
|
||||
- _haversineKm
|
||||
- _isActiveRideState
|
||||
- _isStateProcessing
|
||||
- _isUsingFallback
|
||||
- _kmToLatDelta
|
||||
- _kmToLngDelta
|
||||
- _reconnectTimer
|
||||
- _relevanceScore
|
||||
- a
|
||||
- aerialDistance
|
||||
- apiDistanceMeters
|
||||
- apiKey
|
||||
- attemptCount
|
||||
- c
|
||||
- carInfo
|
||||
- cardNumber
|
||||
- carsOrder
|
||||
- checkPassengerLocation
|
||||
- commissionPct
|
||||
- context
|
||||
- coordDestination
|
||||
- currentAttempt
|
||||
- currentCarType
|
||||
- currentLocationOfDrivers
|
||||
- currentRideId
|
||||
- currentTimeSearchingCaptainWindow
|
||||
- dInfo
|
||||
- dLat
|
||||
- dataCarsLocationByPassenger
|
||||
- datadriverCarsLocationToPassengerAfterApplied
|
||||
- dest
|
||||
- deviation
|
||||
- distanceOfTrip
|
||||
- driverCarPlate
|
||||
- driverLocationToPassenger
|
||||
- driverName
|
||||
- driverOrderStatus
|
||||
- driverPhone
|
||||
- durationByPassenger
|
||||
- dynamicApiUrl
|
||||
- etaText
|
||||
- fName
|
||||
- finalReason
|
||||
- firebaseMessagesController
|
||||
- increaseFeeFormKey
|
||||
- info
|
||||
- isBeginRideFromDriverRunning
|
||||
- isDrawingRoute
|
||||
- isDriversDataValid
|
||||
- isDriversTokensSend
|
||||
- isInUniversity
|
||||
- isRequestValid
|
||||
- kDurationScalar
|
||||
- key
|
||||
- km
|
||||
- kmInDegree
|
||||
- lName
|
||||
- latDest
|
||||
- latestWaypoint
|
||||
- lngDest
|
||||
- lowPerf
|
||||
- mapAPIKEY
|
||||
- messagesFormKey
|
||||
- might
|
||||
- minBillableKm
|
||||
- minFareSYP
|
||||
- newValue
|
||||
- northeast
|
||||
- originCoords
|
||||
- pLower
|
||||
- passengerLocation
|
||||
- passengerLocationStringUnvirsity
|
||||
- placeName
|
||||
- polylineString
|
||||
- previousLocationOfDrivers
|
||||
- progressTimerRideBeginVip
|
||||
- promoFormKey
|
||||
- qLower
|
||||
- query
|
||||
- rLat1
|
||||
- rLat2
|
||||
- ram
|
||||
- rideData
|
||||
- sdk
|
||||
- selectedPassengerCount
|
||||
- southwest
|
||||
- startLng
|
||||
- status
|
||||
- stringElapsedTimeRideBegin
|
||||
@@ -85,11 +85,13 @@ class ComplaintController extends GetxController {
|
||||
var uri = Uri.parse('${AppLink.server}/upload_audio.php');
|
||||
var request = http.MultipartRequest('POST', uri);
|
||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||
final String fingerPrint = box.read(BoxName.deviceFpEncrypted)?.toString() ?? '';
|
||||
|
||||
var mimeType = lookupMimeType(audioFile.path);
|
||||
// ** التعديل: تم استخدام نفس هيدر التوثيق **
|
||||
request.headers.addAll({
|
||||
'Authorization': 'Bearer $token',
|
||||
'X-Device-FP': fingerPrint,
|
||||
});
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath(
|
||||
|
||||
@@ -4,8 +4,7 @@ import 'dart:convert';
|
||||
import 'package:Intaleq/constant/box_name.dart';
|
||||
import 'package:Intaleq/constant/colors.dart';
|
||||
import 'package:Intaleq/constant/links.dart';
|
||||
import 'package:Intaleq/constant/style.dart';
|
||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:Intaleq/main.dart';
|
||||
import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
||||
import 'package:Intaleq/views/widgets/my_scafold.dart';
|
||||
@@ -15,6 +14,7 @@ import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../constant/style.dart';
|
||||
import '../functions/crud.dart';
|
||||
import '../functions/encrypt_decrypt.dart';
|
||||
|
||||
@@ -42,7 +42,7 @@ class VipOrderController extends GetxController {
|
||||
Future<void> fetchOrder() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
var mapPassengerController = Get.find<MapPassengerController>();
|
||||
var mapPassengerController = Get.find<RideLifecycleController>();
|
||||
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getMishwari,
|
||||
@@ -239,7 +239,7 @@ class VipWaittingPage extends StatelessWidget {
|
||||
title: "Cancel Trip".tr,
|
||||
kolor: AppColor.redColor,
|
||||
onPressed: () {
|
||||
Get.find<MapPassengerController>().cancelVip(
|
||||
Get.find<RideLifecycleController>().cancelVip(
|
||||
data['token'].toString(),
|
||||
data['id'].toString(),
|
||||
);
|
||||
@@ -272,7 +272,7 @@ class VipWaittingPage extends StatelessWidget {
|
||||
title: "Send to Driver Again".tr,
|
||||
kolor: AppColor.greenColor,
|
||||
onPressed: () {
|
||||
Get.find<MapPassengerController>()
|
||||
Get.find<RideLifecycleController>()
|
||||
.sendToDriverAgain(data['token']);
|
||||
vipOrderController.fetchOrder();
|
||||
},
|
||||
@@ -292,7 +292,7 @@ class VipWaittingPage extends StatelessWidget {
|
||||
kolor: AppColor.greenColor,
|
||||
onPressed: () {
|
||||
final mapPassengerController =
|
||||
Get.find<MapPassengerController>();
|
||||
Get.find<RideLifecycleController>();
|
||||
mapPassengerController.make = data['make'];
|
||||
mapPassengerController.licensePlate =
|
||||
data['car_plate'];
|
||||
|
||||
@@ -24,6 +24,7 @@ class MyTranslation extends Translations {
|
||||
"4 Passengers": "٤ ركاب",
|
||||
"2. Attach Recorded Audio (Optional)":
|
||||
"٢. إرفاق تسجيل صوتي (اختياري)",
|
||||
"Accept": "قبول",
|
||||
"Account": "الحساب",
|
||||
"Actions": "الإجراءات",
|
||||
"Active Users": "المستخدمين النشطين",
|
||||
@@ -41,9 +42,12 @@ class MyTranslation extends Translations {
|
||||
"Arrived": "وصلنا",
|
||||
"Audio Recording": "تسجيل صوتي",
|
||||
"Call": "اتصال",
|
||||
"Call Connected": "تم فتح الاتصال",
|
||||
"Call Support": "اتصل بالدعم",
|
||||
"Call left": "مكالمات متبقية",
|
||||
"Calling": "عم نتصل بـ",
|
||||
"Change Photo": "تغيير الصورة",
|
||||
"Captain": "الكابتن",
|
||||
"Choose from Gallery": "اختر من المعرض",
|
||||
"Choose from contact": "اختر من جهات الاتصال",
|
||||
"Click to track the trip": "اضغط لتتبع المشوار",
|
||||
@@ -52,11 +56,13 @@ class MyTranslation extends Translations {
|
||||
"Complete Payment": "إتمام الدفع",
|
||||
"Confirm Cancellation": "تأكيد الإلغاء",
|
||||
"Confirm Pickup Location": "تأكيد موقع الانطلاق",
|
||||
"Connecting...": "عم يتم الاتصال...",
|
||||
"Connection failed. Please try again.": "فشل الاتصال. جرب مرة تانية.",
|
||||
"Could not create ride. Please try again.":
|
||||
"ما قدرنا نعمل مشوار. جرب مرة تانية.",
|
||||
"Crop Photo": "قص الصورة",
|
||||
"Dark Mode": "الوضع الليلي",
|
||||
"Decline": "رفض",
|
||||
"Delete": "حذف",
|
||||
"Delete Account": "حذف الحساب",
|
||||
"Delete All": "حذف الكل",
|
||||
@@ -73,6 +79,7 @@ class MyTranslation extends Translations {
|
||||
"Driver is Going To You": "الكابتن جاي لعندك",
|
||||
"Emergency Mode Triggered": "تم تفعيل وضع الطوارئ",
|
||||
"Emergency SOS": "طوارئ SOS",
|
||||
"End": "إنهاء",
|
||||
"Enter the 5-digit code": "أدخل الكود المكون من ٥ أرقام",
|
||||
"Enter your City": "أدخل مدينتك",
|
||||
"Enter your Password": "أدخل كلمة السر",
|
||||
@@ -84,6 +91,7 @@ class MyTranslation extends Translations {
|
||||
"Failed to upload photo": "فشل رفع الصورة",
|
||||
"Finished": "انتهى",
|
||||
"Fixed Price": "سعر ثابت",
|
||||
"Free Call": "مكالمة مجانية",
|
||||
"General": "عام",
|
||||
"Grant": "منح الإذن",
|
||||
"Have a Promo Code?": "معك كود خصم؟",
|
||||
@@ -109,6 +117,7 @@ class MyTranslation extends Translations {
|
||||
"Logout": "تسجيل خروج",
|
||||
"Map Error": "خطأ بالخريطة",
|
||||
"Message": "رسالة",
|
||||
"Mute": "كتم الصوت",
|
||||
"Move map to select destination": "حرك الخريطة لتحديد الوجهة",
|
||||
"Move map to set start location": "حرك الخريطة لتحديد نقطة البداية",
|
||||
"Move map to set stop": "حرك الخريطة لتحديد التوقف",
|
||||
@@ -205,6 +214,7 @@ class MyTranslation extends Translations {
|
||||
"Share your experience to help us improve...":
|
||||
"شاركنا تجربتك لنحسن خدمتنا...",
|
||||
"Something went wrong. Please try again.": "صار غلط. جرب مرة تانية.",
|
||||
"Speaker": "مكبر الصوت",
|
||||
"Speaking...": "عم يحكي...",
|
||||
"Speed Over": "تجاوز السرعة",
|
||||
"Start Point": "نقطة البداية",
|
||||
@@ -254,6 +264,7 @@ class MyTranslation extends Translations {
|
||||
"cancelled": "تكنسل",
|
||||
"due to a previous trip.": "عن مشوار قديم.",
|
||||
"insert sos phone": "دخل رقم طوارئ",
|
||||
"is calling you": "عم يتصل فيك",
|
||||
"is driving a": "عم يسوق",
|
||||
"min added to fare": "دقيقة نضافت للأجرة",
|
||||
"phone not verified": "رقم الموبايل مو متأكد",
|
||||
|
||||
@@ -10,7 +10,9 @@ import 'package:flutter_paypal/flutter_paypal.dart';
|
||||
import 'package:flutter_stripe/flutter_stripe.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_state.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_state.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
@@ -34,7 +36,7 @@ class PaymentController extends GetxController {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
final promo = TextEditingController();
|
||||
final walletphoneController = TextEditingController();
|
||||
double totalPassenger = Get.find<MapPassengerController>().totalPassenger;
|
||||
double totalPassenger = Get.find<RideLifecycleController>().totalPassenger;
|
||||
int? selectedAmount = 0;
|
||||
List<dynamic> totalPassengerWalletDetails = [];
|
||||
String passengerTotalWalletAmount = '';
|
||||
@@ -79,7 +81,7 @@ class PaymentController extends GetxController {
|
||||
|
||||
Future<String> generateTokenDriver(String amount) async {
|
||||
var res = await CRUD().post(link: AppLink.addPaymentTokenDriver, payload: {
|
||||
'driverID': Get.find<MapPassengerController>().driverId,
|
||||
'driverID': Get.find<RideLifecycleController>().driverId,
|
||||
'amount': amount.toString(),
|
||||
});
|
||||
var d = jsonDecode(res);
|
||||
@@ -121,7 +123,7 @@ class PaymentController extends GetxController {
|
||||
'payment_method': 'cancel-from-near',
|
||||
'passengerID': box.read(BoxName.passengerID).toString(),
|
||||
'token': paymentTokenWait,
|
||||
'driverID': Get.find<MapPassengerController>().driverId.toString(),
|
||||
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
|
||||
});
|
||||
var paymentTokenWait1 =
|
||||
await generateTokenDriver(costOfWaiting5Minute.toString());
|
||||
@@ -131,7 +133,7 @@ class PaymentController extends GetxController {
|
||||
'amount': (costOfWaiting5Minute).toStringAsFixed(0),
|
||||
'paymentMethod': 'cancel-from-near',
|
||||
'token': paymentTokenWait1,
|
||||
'driverID': Get.find<MapPassengerController>().driverId.toString(),
|
||||
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
|
||||
});
|
||||
|
||||
if (res != 'failure') {
|
||||
@@ -139,13 +141,13 @@ class PaymentController extends GetxController {
|
||||
// 'Cancel',
|
||||
// 'Trip Cancelled. The cost of the trip will be added to your wallet.'
|
||||
// .tr,
|
||||
// Get.find<MapPassengerController>().driverToken,
|
||||
// Get.find<RideLifecycleController>().driverToken,
|
||||
// [],
|
||||
// 'cancel',
|
||||
// );
|
||||
await NotificationService.sendNotification(
|
||||
category: 'Cancel',
|
||||
target: Get.find<MapPassengerController>().driverToken.toString(),
|
||||
target: Get.find<RideLifecycleController>().driverToken.toString(),
|
||||
title: 'Cancel'.tr,
|
||||
body:
|
||||
'Trip Cancelled. The cost of the trip will be added to your wallet.'
|
||||
@@ -226,7 +228,7 @@ class PaymentController extends GetxController {
|
||||
var firstElement = decod["message"][0];
|
||||
totalPassenger = totalPassenger -
|
||||
(totalPassenger * int.parse(firstElement['amount']));
|
||||
MapPassengerController().promoTaken = true;
|
||||
Get.find<RideLifecycleController>().promoTaken = true;
|
||||
update();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:Intaleq/constant/box_name.dart';
|
||||
import 'package:Intaleq/constant/links.dart';
|
||||
import 'package:Intaleq/constant/style.dart';
|
||||
import 'package:Intaleq/controller/functions/crud.dart';
|
||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:Intaleq/main.dart';
|
||||
import 'package:Intaleq/views/home/map_page_passenger.dart';
|
||||
import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
||||
@@ -45,14 +45,14 @@ class RateController extends GetxController {
|
||||
confirm: MyElevatedButton(title: 'Ok', onPressed: () => Get.back()));
|
||||
} else if (Get.find<PaymentController>().isWalletChecked == true) {
|
||||
double tip = 0;
|
||||
tip = (Get.find<MapPassengerController>().totalPassenger) *
|
||||
tip = (Get.find<RideLifecycleController>().totalPassenger) *
|
||||
(double.parse(box.read(BoxName.tipPercentage).toString()));
|
||||
|
||||
if (tip > 0) {
|
||||
var res = await CRUD().post(link: AppLink.addTips, payload: {
|
||||
'passengerID': box.read(BoxName.passengerID),
|
||||
'driverID': Get.find<MapPassengerController>().driverId.toString(),
|
||||
'rideID': Get.find<MapPassengerController>().rideId.toString(),
|
||||
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
|
||||
'rideID': Get.find<RideLifecycleController>().rideId.toString(),
|
||||
'tipAmount': tip.toString(),
|
||||
});
|
||||
await Get.find<PaymentController>()
|
||||
@@ -62,8 +62,8 @@ class RateController extends GetxController {
|
||||
? tip.toStringAsFixed(0)
|
||||
: (tip * 100).toString());
|
||||
await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: {
|
||||
'driverID': Get.find<MapPassengerController>().driverId.toString(),
|
||||
'paymentID': '${Get.find<MapPassengerController>().rideId}tip',
|
||||
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
|
||||
'paymentID': '${Get.find<RideLifecycleController>().rideId}tip',
|
||||
'amount': box.read(BoxName.countryCode) == 'Egypt'
|
||||
? tip.toStringAsFixed(0)
|
||||
: (tip * 100).toString(),
|
||||
@@ -73,10 +73,10 @@ class RateController extends GetxController {
|
||||
if (res != 'failure') {
|
||||
await NotificationService.sendNotification(
|
||||
category: 'You Have Tips',
|
||||
target: Get.find<MapPassengerController>().driverToken.toString(),
|
||||
target: Get.find<RideLifecycleController>().driverToken.toString(),
|
||||
title: 'You Have Tips'.tr,
|
||||
body:
|
||||
'${'${tip.toString()}\$${' tips\nTotal is'.tr}'} ${tip + (Get.find<MapPassengerController>().totalPassenger)}',
|
||||
'${'${tip.toString()}\$${' tips\nTotal is'.tr}'} ${tip + (Get.find<RideLifecycleController>().totalPassenger)}',
|
||||
isTopic: false, // Important: this is a token
|
||||
tone: 'ding',
|
||||
driverList: [],
|
||||
@@ -95,7 +95,7 @@ class RateController extends GetxController {
|
||||
},
|
||||
);
|
||||
|
||||
Get.find<MapPassengerController>().restCounter();
|
||||
Get.find<RideLifecycleController>().restCounter();
|
||||
Get.offAll(const MapPagePassenger());
|
||||
}
|
||||
}
|
||||
|
||||
722
lib/controller/voice_call_controller.dart
Normal file
722
lib/controller/voice_call_controller.dart
Normal file
@@ -0,0 +1,722 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||
import 'package:get/get.dart' hide Response;
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../../services/signaling_service.dart';
|
||||
import '../../views/widgets/voice_call_bottom_sheet.dart';
|
||||
import 'functions/crud.dart';
|
||||
|
||||
// EN: Enum representing the different states of a voice call.
|
||||
// AR: تعداد يمثل الحالات المختلفة للمكالمة الصوتية.
|
||||
enum VoiceCallState { idle, dialing, ringing, connecting, active, ended }
|
||||
|
||||
class VoiceCallController extends GetxController with WidgetsBindingObserver {
|
||||
// EN: Instance of the signaling service to manage WebSocket communication.
|
||||
// AR: مثيل لخدمة الإشارات لإدارة الاتصال عبر الـ WebSocket.
|
||||
final SignalingService _signaling = SignalingService();
|
||||
|
||||
// --- Observable Variables (GetX) / المتغيرات التفاعلية ---
|
||||
|
||||
// EN: Current state of the call.
|
||||
// AR: الحالة الحالية للمكالمة.
|
||||
var state = VoiceCallState.idle.obs;
|
||||
|
||||
// EN: Unique identifier for the WebRTC session.
|
||||
// AR: المعرف الفريد لجلسة الاتصال.
|
||||
var sessionId = "".obs;
|
||||
|
||||
// EN: ID of the current active ride.
|
||||
// AR: معرف الرحلة النشطة الحالية.
|
||||
var rideId = "".obs;
|
||||
|
||||
// EN: Name of the other party (Driver/Passenger).
|
||||
// AR: اسم الطرف الآخر في المكالمة (سائق/راكب).
|
||||
var remoteName = "User".obs;
|
||||
|
||||
// EN: Microphone mute status.
|
||||
// AR: حالة كتم الميكروفون.
|
||||
var isMuted = false.obs;
|
||||
|
||||
// EN: Speakerphone status.
|
||||
// AR: حالة مكبر الصوت الخارجي.
|
||||
var isSpeakerOn = false.obs;
|
||||
|
||||
// EN: Timer countdown variable, starts from 60 seconds.
|
||||
// AR: متغير العد التنازلي للمؤقت، يبدأ من 60 ثانية.
|
||||
var elapsedSeconds = 60.obs;
|
||||
|
||||
// --- Core State Variables / متغيرات الحالة الأساسية ---
|
||||
|
||||
// EN: Flag to determine if the current user initiated the call.
|
||||
// AR: مؤشر لتحديد ما إذا كان المستخدم الحالي هو من بدأ المكالمة.
|
||||
bool isCaller = false;
|
||||
|
||||
// EN: ID of the current user.
|
||||
// AR: معرف المستخدم الحالي.
|
||||
String currentUserId = "";
|
||||
|
||||
// --- WebRTC Internal Variables / متغيرات WebRTC الداخلية ---
|
||||
|
||||
// EN: The main connection object between peers.
|
||||
// AR: كائن الاتصال الرئيسي بين الطرفين.
|
||||
rtc.RTCPeerConnection? _peerConnection;
|
||||
|
||||
// EN: The local audio stream captured from the microphone.
|
||||
// AR: دفق الصوت المحلي الملتقط من الميكروفون.
|
||||
rtc.MediaStream? _localStream;
|
||||
|
||||
// EN: Timer to enforce the 60-second call limit.
|
||||
// AR: مؤقت لفرض حد الـ 60 ثانية للمكالمة.
|
||||
Timer? _countdownTimer;
|
||||
|
||||
// EN: Timer to hang up if the call is not answered within 30 seconds.
|
||||
// AR: مؤقت لإنهاء المكالمة إذا لم يتم الرد خلال 30 ثانية.
|
||||
Timer? _ringingTimeoutTimer;
|
||||
|
||||
// EN: Flag to indicate if the peer connection is currently attempting ICE reconnection.
|
||||
// AR: مؤشر يوضح ما إذا كان الاتصال يحاول إعادة بناء مسارات الشبكة حالياً.
|
||||
bool _isReconnecting = false;
|
||||
Timer? _reconnectTimer;
|
||||
List<dynamic> _dynamicIceServers = [];
|
||||
|
||||
AudioPlayer? _ringtonePlayer;
|
||||
|
||||
void _startRingtone() async {
|
||||
try {
|
||||
_ringtonePlayer ??= AudioPlayer();
|
||||
await _ringtonePlayer!.setAsset('assets/start.wav');
|
||||
await _ringtonePlayer!.setLoopMode(LoopMode.one);
|
||||
_ringtonePlayer!.play();
|
||||
} catch (e) {
|
||||
Log.print("Error playing ringtone: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void _stopRingtone() {
|
||||
try {
|
||||
_ringtonePlayer?.stop();
|
||||
} catch (e) {
|
||||
Log.print("Error stopping ringtone: $e");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// EN: Add lifecycle observer.
|
||||
// AR: إضافة مراقب لدورة حياة التطبيق.
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
// EN: Initialize WebSocket signaling listeners.
|
||||
// AR: تهيئة مستمعي إشارات الـ WebSocket.
|
||||
_initSignalingCallbacks();
|
||||
}
|
||||
|
||||
// EN: Lifecycle hook: handle app switching background/foreground.
|
||||
// AR: معالجة انتقال التطبيق إلى الخلفية أو العودة للواجهة.
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
Log.print("VoiceCall: didChangeAppLifecycleState -> $state");
|
||||
if (state == AppLifecycleState.paused) {
|
||||
Log.print(
|
||||
"WARNING: App is in background. Microphone access might be suspended by the OS.",
|
||||
);
|
||||
} else if (state == AppLifecycleState.resumed) {
|
||||
Log.print("App resumed. Verifying WebRTC connection health.");
|
||||
if (this.state.value == VoiceCallState.active) {
|
||||
_ensureMicrophoneActive();
|
||||
_attemptIceRestart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EN: Registers all event listeners for the signaling server.
|
||||
// AR: تسجيل جميع مستمعي الأحداث لخادم الإشارات.
|
||||
void _initSignalingCallbacks() {
|
||||
// EN: Triggered when successfully connected to the signaling server.
|
||||
// AR: يُستدعى عند الاتصال بنجاح بخادم الإشارات.
|
||||
_signaling.onConnected = (iceServers) {
|
||||
Log.print("WebRTC Signaling Connected & Authenticated");
|
||||
_dynamicIceServers = iceServers;
|
||||
};
|
||||
|
||||
// EN: Triggered when the WebSocket connection drops.
|
||||
// AR: يُستدعى عند انقطاع اتصال الـ WebSocket.
|
||||
_signaling.onDisconnected = (reason) {
|
||||
Log.print("WebRTC Signaling Disconnected: $reason");
|
||||
if (state.value != VoiceCallState.idle) {
|
||||
_endCallInternal("signaling_disconnected");
|
||||
}
|
||||
};
|
||||
|
||||
// EN: Triggered when the remote user joins the room.
|
||||
// AR: يُستدعى عند انضمام الطرف الآخر إلى غرفة الاتصال.
|
||||
_signaling.onParticipantJoined = () async {
|
||||
Log.print("Remote participant joined signaling session");
|
||||
// EN: If we are the caller, initiate the WebRTC handshake by creating an Offer.
|
||||
// AR: إذا كنا نحن المتصل، نبدأ مصافحة WebRTC بإنشاء عرض (Offer).
|
||||
if (isCaller && state.value == VoiceCallState.dialing) {
|
||||
state.value = VoiceCallState.connecting;
|
||||
await _createOffer();
|
||||
}
|
||||
};
|
||||
|
||||
// EN: Triggered when an SDP Offer is received from the remote peer.
|
||||
// AR: يُستدعى عند استلام عرض اتصال (Offer) من الطرف الآخر.
|
||||
_signaling.onOffer = (sdpMap) async {
|
||||
Log.print("Received WebRTC SDP Offer");
|
||||
if (!isCaller) {
|
||||
state.value = VoiceCallState.connecting;
|
||||
await _initializePeerConnection();
|
||||
|
||||
// EN: Set the remote peer's settings.
|
||||
// AR: تعيين إعدادات الطرف الآخر.
|
||||
final description = rtc.RTCSessionDescription(
|
||||
sdpMap['sdp'],
|
||||
sdpMap['type'],
|
||||
);
|
||||
await _peerConnection!.setRemoteDescription(description);
|
||||
|
||||
// EN: Respond with an Answer.
|
||||
// AR: الرد بإجابة (Answer).
|
||||
await _createAnswer();
|
||||
}
|
||||
};
|
||||
|
||||
// EN: Triggered when an SDP Answer is received.
|
||||
// AR: يُستدعى عند استلام إجابة (Answer) من الطرف الآخر.
|
||||
_signaling.onAnswer = (sdpMap) async {
|
||||
Log.print("Received WebRTC SDP Answer");
|
||||
if (isCaller && _peerConnection != null) {
|
||||
final description = rtc.RTCSessionDescription(
|
||||
sdpMap['sdp'],
|
||||
sdpMap['type'],
|
||||
);
|
||||
await _peerConnection!.setRemoteDescription(description);
|
||||
}
|
||||
};
|
||||
|
||||
// EN: Triggered when ICE candidates (Network routing info) are exchanged.
|
||||
// AR: يُستدعى عند تبادل مسارات الشبكة (ICE Candidates) لتأسيس الاتصال.
|
||||
_signaling.onIceCandidate = (candidateMap) async {
|
||||
Log.print("Received Remote ICE Candidate");
|
||||
if (_peerConnection != null) {
|
||||
final candidate = rtc.RTCIceCandidate(
|
||||
candidateMap['candidate'],
|
||||
candidateMap['sdpMid'],
|
||||
candidateMap['sdpMLineIndex'],
|
||||
);
|
||||
await _peerConnection!.addCandidate(candidate);
|
||||
}
|
||||
};
|
||||
|
||||
// EN: Triggered when a hangup event is received from the server.
|
||||
// AR: يُستدعى عند استلام حدث إنهاء المكالمة من السيرفر.
|
||||
_signaling.onCallEnded = (reason) {
|
||||
Log.print("WebRTC Call Ended: $reason");
|
||||
_endCallInternal(reason);
|
||||
};
|
||||
}
|
||||
|
||||
// --- CALL LIFECYCLE / دورة حياة المكالمة ---
|
||||
|
||||
// EN: Initiates an outgoing call.
|
||||
// AR: يبدأ مكالمة صادرة.
|
||||
Future<void> startCall({
|
||||
required String rideIdVal,
|
||||
required String driverId,
|
||||
required String passengerId,
|
||||
required String remoteNameVal,
|
||||
}) async {
|
||||
if (state.value != VoiceCallState.idle) return;
|
||||
|
||||
// EN: Setup call variables.
|
||||
// AR: إعداد متغيرات المكالمة.
|
||||
state.value = VoiceCallState.dialing;
|
||||
isCaller = true;
|
||||
currentUserId = passengerId;
|
||||
rideId.value = rideIdVal;
|
||||
remoteName.value = remoteNameVal;
|
||||
isMuted.value = false;
|
||||
isSpeakerOn.value = false;
|
||||
elapsedSeconds.value = 60;
|
||||
_isReconnecting = false;
|
||||
|
||||
_showCallBottomSheet();
|
||||
HapticFeedback.vibrate();
|
||||
|
||||
try {
|
||||
// 1. EN: Request Microphone Permission / AR: طلب صلاحية الميكروفون
|
||||
final permissionStatus = await Permission.microphone.request();
|
||||
if (!permissionStatus.isGranted) {
|
||||
_endCallInternal("permission_denied");
|
||||
Get.snackbar(
|
||||
"Error",
|
||||
"Microphone permission is required for voice calls".tr,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. EN: Call PHP Backend to create Node.js session & notify Driver via FCM.
|
||||
// AR: استدعاء واجهة PHP لإنشاء الجلسة على Node.js وإشعار السائق عبر FCM.
|
||||
final response = await CRUD().post(
|
||||
link: "${AppLink.server}/ride/call/passenger/create_call_session.php",
|
||||
payload: {'ride_id': rideIdVal},
|
||||
);
|
||||
|
||||
if (response == null ||
|
||||
response == 'failure' ||
|
||||
response['status'] != 'success') {
|
||||
_endCallInternal("session_creation_failed");
|
||||
Get.snackbar(
|
||||
"Error",
|
||||
"Failed to initiate call session. Please try again.".tr,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final data = response['data'];
|
||||
sessionId.value = data['session_id'];
|
||||
|
||||
// 3. EN: Connect to WebRTC signaling server / AR: الاتصال بخادم الإشارات
|
||||
await _signaling.connect(sessionId.value, currentUserId);
|
||||
|
||||
// 4. EN: Initialize Local WebRTC Audio Stream / AR: تهيئة دفق الصوت المحلي
|
||||
await _initializeLocalStream();
|
||||
|
||||
// 5. EN: Start Ringing Timeout Timer (30s max wait for driver to answer).
|
||||
// AR: بدء مؤقت الرنين (أقصى انتظار 30 ثانية لرد السائق).
|
||||
_ringingTimeoutTimer = Timer(const Duration(seconds: 30), () {
|
||||
if (state.value == VoiceCallState.dialing) {
|
||||
_signaling.send("hangup", {"reason": "no_answer"});
|
||||
_endCallInternal("no_answer");
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
Log.print("Error starting WebRTC call: $e");
|
||||
_endCallInternal("error");
|
||||
}
|
||||
}
|
||||
|
||||
// EN: Handles incoming call requests via FCM/Socket.
|
||||
// AR: معالجة طلبات المكالمات الواردة.
|
||||
Future<void> receiveCall({
|
||||
required String sessionIdVal,
|
||||
required String remoteNameVal,
|
||||
required String rideIdVal,
|
||||
}) async {
|
||||
// EN: If already in a call, send busy signal.
|
||||
// AR: إذا كان في مكالمة بالفعل، إرسال إشارة مشغول.
|
||||
if (state.value != VoiceCallState.idle) {
|
||||
_signaling.send("hangup", {"reason": "busy"});
|
||||
return;
|
||||
}
|
||||
|
||||
state.value = VoiceCallState.ringing;
|
||||
isCaller = false;
|
||||
currentUserId = box.read(BoxName.passengerID).toString();
|
||||
sessionId.value = sessionIdVal;
|
||||
rideId.value = rideIdVal;
|
||||
remoteName.value = remoteNameVal;
|
||||
isMuted.value = false;
|
||||
isSpeakerOn.value = false;
|
||||
elapsedSeconds.value = 60;
|
||||
_isReconnecting = false;
|
||||
|
||||
_showCallBottomSheet();
|
||||
_startRingtone();
|
||||
HapticFeedback.vibrate();
|
||||
|
||||
// EN: Max 30s ringing timeout for receiver before auto-decline.
|
||||
// AR: أقصى مدة للرنين 30 ثانية قبل الرفض التلقائي.
|
||||
_ringingTimeoutTimer = Timer(const Duration(seconds: 30), () {
|
||||
if (state.value == VoiceCallState.ringing) {
|
||||
declineCall();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// EN: Accepts the incoming call.
|
||||
// AR: قبول المكالمة الواردة.
|
||||
Future<void> acceptCall() async {
|
||||
if (state.value != VoiceCallState.ringing) return;
|
||||
|
||||
_ringingTimeoutTimer?.cancel();
|
||||
_stopRingtone();
|
||||
state.value = VoiceCallState.connecting;
|
||||
|
||||
try {
|
||||
// EN: Check Mic permissions / AR: التحقق من صلاحيات الميكروفون
|
||||
final permissionStatus = await Permission.microphone.request();
|
||||
if (!permissionStatus.isGranted) {
|
||||
declineCall();
|
||||
Get.snackbar(
|
||||
"Error",
|
||||
"Microphone permission is required for voice calls".tr,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await _signaling.connect(sessionId.value, currentUserId);
|
||||
await _initializeLocalStream();
|
||||
|
||||
// EN: Notify caller we accepted / AR: إشعار المتصل بأننا قبلنا المكالمة
|
||||
_signaling.send("join", {});
|
||||
} catch (e) {
|
||||
Log.print("Error accepting call: $e");
|
||||
declineCall();
|
||||
}
|
||||
}
|
||||
|
||||
// EN: Declines an incoming call.
|
||||
// AR: رفض المكالمة الواردة.
|
||||
void declineCall() {
|
||||
_ringingTimeoutTimer?.cancel();
|
||||
_stopRingtone();
|
||||
_signaling.send("hangup", {"reason": "declined"});
|
||||
_endCallInternal("declined");
|
||||
}
|
||||
|
||||
// EN: Ends an active or dialing call.
|
||||
// AR: إنهاء المكالمة النشطة أو الجاري الاتصال بها.
|
||||
void hangup() {
|
||||
_signaling.send("hangup", {"reason": "normal"});
|
||||
_endCallInternal("hangup");
|
||||
}
|
||||
|
||||
// --- WEBRTC CORE HELPERS / دوال WebRTC الأساسية ---
|
||||
|
||||
// EN: Captures the audio from the microphone with optimization constraints.
|
||||
// AR: التقاط الصوت من الميكروفون مع قيود تحسين الجودة (إلغاء الصدى والضوضاء).
|
||||
Future<void> _initializeLocalStream() async {
|
||||
final Map<String, dynamic> mediaConstraints = {
|
||||
'audio': {
|
||||
'echoCancellation': true,
|
||||
'noiseSuppression': true,
|
||||
'autoGainControl': true,
|
||||
},
|
||||
'video': false, // EN: Audio only / AR: صوت فقط
|
||||
};
|
||||
|
||||
_localStream = await rtc.navigator.mediaDevices.getUserMedia(
|
||||
mediaConstraints,
|
||||
);
|
||||
rtc.Helper.setSpeakerphoneOn(isSpeakerOn.value);
|
||||
}
|
||||
|
||||
// EN: Verifies local microphone stream health on app resume and recreates/replaces track if suspended.
|
||||
// AR: التحقق من سلامة مسار الميكروفون المحلي عند استئناف التطبيق وإعادة إنشائه إذا تم تعليقه.
|
||||
Future<void> _ensureMicrophoneActive() async {
|
||||
if (_localStream == null || _peerConnection == null) return;
|
||||
|
||||
bool needsRecreation = false;
|
||||
if (_localStream!.active == false) {
|
||||
needsRecreation = true;
|
||||
} else {
|
||||
for (var track in _localStream!.getAudioTracks()) {
|
||||
if (!track.enabled && !isMuted.value) {
|
||||
needsRecreation = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsRecreation) {
|
||||
Log.print(
|
||||
"Local audio track ended or disabled. Recreating local stream...",
|
||||
);
|
||||
try {
|
||||
_localStream?.getTracks().forEach((track) => track.stop());
|
||||
_localStream?.dispose();
|
||||
_localStream = null;
|
||||
|
||||
await _initializeLocalStream();
|
||||
|
||||
final senders = await _peerConnection!.getSenders();
|
||||
for (var sender in senders) {
|
||||
final track = sender.track;
|
||||
if (track != null && track.kind == 'audio') {
|
||||
final newTracks = _localStream?.getAudioTracks();
|
||||
if (newTracks != null && newTracks.isNotEmpty) {
|
||||
await sender.replaceTrack(newTracks.first);
|
||||
Log.print(
|
||||
"Replaced suspended/ended audio track with a new active one.",
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Error recreating local stream on resume: $e");
|
||||
}
|
||||
} else {
|
||||
_localStream!.getAudioTracks().forEach((track) {
|
||||
track.enabled = !isMuted.value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// EN: Creates the peer connection object and sets up ICE servers (STUN/TURN).
|
||||
// AR: إنشاء كائن الاتصال المباشر وإعداد خوادم STUN/TURN لاختراق الجدران النارية.
|
||||
Future<void> _initializePeerConnection() async {
|
||||
if (_peerConnection != null) return;
|
||||
|
||||
final List<Map<String, dynamic>> iceServers = [];
|
||||
if (_dynamicIceServers.isNotEmpty) {
|
||||
for (var server in _dynamicIceServers) {
|
||||
if (server is Map) {
|
||||
iceServers.add({
|
||||
"urls": server["urls"] ?? server["url"],
|
||||
if (server["username"] != null) "username": server["username"],
|
||||
if (server["credential"] != null)
|
||||
"credential": server["credential"],
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// EN: Fallback STUN servers / AR: خوادم STUN الاحتياطية
|
||||
iceServers.addAll([
|
||||
{"urls": "stun:stun.l.google.com:19302"},
|
||||
{"urls": "stun:stun1.l.google.com:19302"},
|
||||
]);
|
||||
}
|
||||
|
||||
final Map<String, dynamic> configuration = {"iceServers": iceServers};
|
||||
|
||||
_peerConnection = await rtc.createPeerConnection(configuration);
|
||||
|
||||
// EN: Gather local network routing info and send to remote peer.
|
||||
// AR: جمع بيانات مسارات الشبكة المحلية وإرسالها للطرف الآخر.
|
||||
_peerConnection!.onIceCandidate = (candidate) {
|
||||
if (candidate.candidate != null) {
|
||||
_signaling.send("ice_candidate", {
|
||||
"candidate": {
|
||||
"candidate": candidate.candidate,
|
||||
"sdpMid": candidate.sdpMid,
|
||||
"sdpMLineIndex": candidate.sdpMLineIndex,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// EN: Monitor connection status changes and handle disconnections.
|
||||
// AR: مراقبة تغيرات حالة الاتصال ومعالجة انقطاع الشبكة.
|
||||
_peerConnection!.onConnectionState = (connState) {
|
||||
Log.print("RTCPeerConnectionState: $connState");
|
||||
if (connState ==
|
||||
rtc.RTCPeerConnectionState.RTCPeerConnectionStateConnected) {
|
||||
_onCallConnected();
|
||||
} else if (connState ==
|
||||
rtc.RTCPeerConnectionState.RTCPeerConnectionStateFailed ||
|
||||
connState ==
|
||||
rtc.RTCPeerConnectionState.RTCPeerConnectionStateDisconnected) {
|
||||
_handleIceConnectionFailure();
|
||||
}
|
||||
};
|
||||
|
||||
// EN: Add local audio stream to the connection to send it to the other peer.
|
||||
// AR: إضافة دفق الصوت المحلي للاتصال لإرساله للطرف الآخر.
|
||||
if (_localStream != null) {
|
||||
_localStream!.getTracks().forEach((track) {
|
||||
_peerConnection!.addTrack(track, _localStream!);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// EN: Attempts an ICE restart to reconnect the WebRTC session when disconnections occur.
|
||||
// AR: محاولة إعادة تأسيس الاتصال (ICE Restart) في حالة انقطاع الشبكة.
|
||||
void _handleIceConnectionFailure() {
|
||||
if (_isReconnecting) return;
|
||||
_isReconnecting = true;
|
||||
Log.print(
|
||||
"ICE connection dropped. Attempting ICE Restart reconnection for 5s...",
|
||||
);
|
||||
|
||||
if (isCaller) {
|
||||
_attemptIceRestart();
|
||||
}
|
||||
|
||||
_reconnectTimer?.cancel();
|
||||
_reconnectTimer = Timer(const Duration(seconds: 5), () {
|
||||
if (state.value == VoiceCallState.active &&
|
||||
_peerConnection?.connectionState !=
|
||||
rtc.RTCPeerConnectionState.RTCPeerConnectionStateConnected) {
|
||||
Log.print("ICE reconnection timed out. Hanging up.");
|
||||
_endCallInternal("connection_lost");
|
||||
} else {
|
||||
_isReconnecting = false;
|
||||
Log.print("ICE Reconnection succeeded!");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// EN: Initiates ICE Restart SDP exchange.
|
||||
// AR: بدء تبادل حزم SDP لإعادة بناء مسارات الاتصال.
|
||||
Future<void> _attemptIceRestart() async {
|
||||
if (_peerConnection == null || !isCaller) return;
|
||||
try {
|
||||
Log.print("Caller initiating WebRTC ICE Restart...");
|
||||
final constraints = {
|
||||
'mandatory': {
|
||||
'OfferToReceiveAudio': true,
|
||||
'OfferToReceiveVideo': false,
|
||||
},
|
||||
'optional': [
|
||||
{'IceRestart': true},
|
||||
],
|
||||
};
|
||||
final offer = await _peerConnection!.createOffer(constraints);
|
||||
await _peerConnection!.setLocalDescription(offer);
|
||||
_signaling.send("offer", {
|
||||
"sdp": {"sdp": offer.sdp, "type": offer.type},
|
||||
});
|
||||
} catch (e) {
|
||||
Log.print("Error initiating WebRTC ICE Restart: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// EN: Generates an SDP Offer to initialize the connection.
|
||||
// AR: إنشاء عرض (Offer) لبدء الاتصال وتحديد قدرات الجهاز.
|
||||
Future<void> _createOffer() async {
|
||||
await _initializePeerConnection();
|
||||
|
||||
final constraints = {
|
||||
'mandatory': {'OfferToReceiveAudio': true, 'OfferToReceiveVideo': false},
|
||||
'optional': [],
|
||||
};
|
||||
|
||||
final offer = await _peerConnection!.createOffer(constraints);
|
||||
await _peerConnection!.setLocalDescription(offer);
|
||||
|
||||
_signaling.send("offer", {
|
||||
"sdp": {"sdp": offer.sdp, "type": offer.type},
|
||||
});
|
||||
}
|
||||
|
||||
// EN: Generates an SDP Answer in response to an Offer.
|
||||
// AR: الرد بإنشاء إجابة (Answer) بناءً على العرض المستلم.
|
||||
Future<void> _createAnswer() async {
|
||||
final constraints = {
|
||||
'mandatory': {'OfferToReceiveAudio': true, 'OfferToReceiveVideo': false},
|
||||
'optional': [],
|
||||
};
|
||||
|
||||
final answer = await _peerConnection!.createAnswer(constraints);
|
||||
await _peerConnection!.setLocalDescription(answer);
|
||||
|
||||
_signaling.send("answer", {
|
||||
"sdp": {"sdp": answer.sdp, "type": answer.type},
|
||||
});
|
||||
}
|
||||
|
||||
// EN: Triggered when connection is fully established. Starts the 60s timer.
|
||||
// AR: يُستدعى عند تأسيس الاتصال بنجاح، ويقوم ببدء مؤقت الـ 60 ثانية.
|
||||
void _onCallConnected() {
|
||||
_ringingTimeoutTimer?.cancel();
|
||||
_reconnectTimer?.cancel();
|
||||
_isReconnecting = false;
|
||||
|
||||
if (state.value != VoiceCallState.active) {
|
||||
state.value = VoiceCallState.active;
|
||||
HapticFeedback.vibrate();
|
||||
|
||||
// EN: Start 120s countdown timer / AR: بدء العد التنازلي لمدة 120 ثانية
|
||||
_countdownTimer?.cancel();
|
||||
elapsedSeconds.value = 120;
|
||||
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
if (elapsedSeconds.value > 1) {
|
||||
elapsedSeconds.value--;
|
||||
} else {
|
||||
elapsedSeconds.value = 0;
|
||||
_countdownTimer?.cancel();
|
||||
// EN: Force hangup when timer reaches 0 / AR: إغلاق إجباري عند وصول المؤقت لصفر
|
||||
hangup();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// EN: Internal cleanup function. Closes all connections and streams.
|
||||
// AR: دالة التنظيف الداخلية. تقوم بإغلاق جميع الاتصالات وتفريغ الذاكرة.
|
||||
void _endCallInternal(String reason) {
|
||||
_countdownTimer?.cancel();
|
||||
_ringingTimeoutTimer?.cancel();
|
||||
_reconnectTimer?.cancel();
|
||||
_stopRingtone();
|
||||
|
||||
state.value = VoiceCallState.ended;
|
||||
|
||||
// EN: Close WebRTC connection / AR: إغلاق اتصال WebRTC
|
||||
_peerConnection?.close();
|
||||
_peerConnection = null;
|
||||
|
||||
// EN: Stop mic capture / AR: إيقاف التقاط الميكروفون
|
||||
_localStream?.getTracks().forEach((track) => track.stop());
|
||||
_localStream?.dispose();
|
||||
_localStream = null;
|
||||
|
||||
// EN: Disconnect WebSockets / AR: إغلاق اتصال الـ WebSockets
|
||||
_signaling.disconnect();
|
||||
|
||||
// EN: Close UI BottomSheet after delay / AR: إغلاق واجهة المكالمة بعد فترة زمنية قصيرة
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
if (state.value == VoiceCallState.ended) {
|
||||
state.value = VoiceCallState.idle;
|
||||
Get.back();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// --- ACTIONS (UI Controls) / إجراءات الواجهة ---
|
||||
|
||||
// EN: Toggles microphone mute state.
|
||||
// AR: تبديل حالة كتم الميكروفون.
|
||||
void toggleMute() {
|
||||
isMuted.value = !isMuted.value;
|
||||
_localStream?.getAudioTracks().forEach((track) {
|
||||
track.enabled = !isMuted.value;
|
||||
});
|
||||
}
|
||||
|
||||
// EN: Toggles loudspeaker mode.
|
||||
// AR: تبديل حالة مكبر الصوت الخارجي.
|
||||
void toggleSpeaker() {
|
||||
isSpeakerOn.value = !isSpeakerOn.value;
|
||||
rtc.Helper.setSpeakerphoneOn(isSpeakerOn.value);
|
||||
}
|
||||
|
||||
// EN: Displays the call UI overlay.
|
||||
// AR: إظهار نافذة المكالمة السفلية.
|
||||
void _showCallBottomSheet() {
|
||||
Get.bottomSheet(
|
||||
const VoiceCallBottomSheet(),
|
||||
isScrollControlled: true,
|
||||
enableDrag: false,
|
||||
isDismissible: false,
|
||||
);
|
||||
}
|
||||
|
||||
// EN: Lifecycle hook: clean up resources when controller is destroyed.
|
||||
// AR: دورة الحياة: تفريغ الذاكرة وإغلاق الموارد عند تدمير المتحكم.
|
||||
@override
|
||||
void onClose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_countdownTimer?.cancel();
|
||||
_ringingTimeoutTimer?.cancel();
|
||||
_reconnectTimer?.cancel();
|
||||
_stopRingtone();
|
||||
_ringtonePlayer?.dispose();
|
||||
_peerConnection?.close();
|
||||
_localStream?.dispose();
|
||||
_signaling.disconnect();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:Intaleq/print.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class WidgetManager {
|
||||
static const platform = MethodChannel('com.mobileapp.store.ride/widget');
|
||||
|
||||
static Future<void> updateWidget() async {
|
||||
try {
|
||||
await platform.invokeMethod('updateWidget');
|
||||
} on PlatformException catch (e) {
|
||||
Log.print("Failed to update widget: '${e.message}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
void updateHomeScreenWidget() {
|
||||
WidgetManager.updateWidget();
|
||||
}
|
||||
111
lib/services/signaling_service.dart
Normal file
111
lib/services/signaling_service.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:Intaleq/print.dart';
|
||||
|
||||
class SignalingService {
|
||||
WebSocket? _socket;
|
||||
final String _url = "wss://calls.intaleqapp.com/ws";
|
||||
|
||||
// Callbacks
|
||||
Function(List<dynamic> iceServers)? onConnected;
|
||||
Function(String reason)? onDisconnected;
|
||||
Function(Map<String, dynamic> offer)? onOffer;
|
||||
Function(Map<String, dynamic> answer)? onAnswer;
|
||||
Function(Map<String, dynamic> candidate)? onIceCandidate;
|
||||
Function(String reason)? onCallEnded;
|
||||
Function()? onParticipantJoined;
|
||||
|
||||
bool get isConnected => _socket != null && _socket!.readyState == WebSocket.open;
|
||||
|
||||
Future<void> connect(String sessionId, String userId) async {
|
||||
if (isConnected) return;
|
||||
|
||||
try {
|
||||
Log.print("Signaling: Connecting to $_url");
|
||||
_socket = await WebSocket.connect(_url)
|
||||
.timeout(const Duration(seconds: 8));
|
||||
|
||||
_socket!.listen(
|
||||
(data) {
|
||||
_handleMessage(data);
|
||||
},
|
||||
onError: (err) {
|
||||
Log.print("Signaling socket error: $err");
|
||||
disconnect("socket_error");
|
||||
},
|
||||
onDone: () {
|
||||
Log.print("Signaling socket closed by server");
|
||||
disconnect("socket_closed");
|
||||
},
|
||||
cancelOnError: true,
|
||||
);
|
||||
|
||||
// Send the authenticate message as the first message
|
||||
send("authenticate", {
|
||||
"session_id": sessionId,
|
||||
"user_id": userId,
|
||||
});
|
||||
} catch (e) {
|
||||
Log.print("Signaling connection failed: $e");
|
||||
onDisconnected?.call("connection_failed");
|
||||
}
|
||||
}
|
||||
|
||||
void _handleMessage(dynamic data) {
|
||||
try {
|
||||
Log.print("Signaling received raw: $data");
|
||||
final message = jsonDecode(data);
|
||||
if (message is! Map<String, dynamic>) return;
|
||||
|
||||
final type = message['type'];
|
||||
switch (type) {
|
||||
case 'authenticated':
|
||||
final iceServers = message['ice_servers'] as List<dynamic>? ?? [];
|
||||
onConnected?.call(iceServers);
|
||||
break;
|
||||
case 'participant_joined':
|
||||
onParticipantJoined?.call();
|
||||
break;
|
||||
case 'offer':
|
||||
if (message['sdp'] != null) {
|
||||
onOffer?.call(message['sdp']);
|
||||
}
|
||||
break;
|
||||
case 'answer':
|
||||
if (message['sdp'] != null) {
|
||||
onAnswer?.call(message['sdp']);
|
||||
}
|
||||
break;
|
||||
case 'ice_candidate':
|
||||
if (message['candidate'] != null) {
|
||||
onIceCandidate?.call(message['candidate']);
|
||||
}
|
||||
break;
|
||||
case 'call_ended':
|
||||
onCallEnded?.call(message['reason'] ?? 'normal');
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("Error handling signaling message: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void send(String type, Map<String, dynamic> data) {
|
||||
if (!isConnected) return;
|
||||
final msg = jsonEncode({
|
||||
'type': type,
|
||||
...data,
|
||||
});
|
||||
Log.print("Signaling sending: $msg");
|
||||
_socket!.add(msg);
|
||||
}
|
||||
|
||||
void disconnect([String reason = "user_hangup"]) {
|
||||
if (_socket != null) {
|
||||
_socket!.close();
|
||||
_socket = null;
|
||||
onDisconnected?.call(reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -40,7 +40,7 @@ class RateDriverFromPassenger extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(
|
||||
'${'Total price to '.tr}${Get.find<MapPassengerController>().driverName}',
|
||||
'${'Total price to '.tr}${Get.find<RideLifecycleController>().driverName}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Row(
|
||||
@@ -189,7 +189,7 @@ class RateDriverFromPassenger extends StatelessWidget {
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color:
|
||||
AppColor.grayColor.withOpacity(0.5)), // Customize the border color
|
||||
AppColor.grayColor.withValues(alpha: 0.5)), // Customize the border color
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
|
||||
@@ -410,7 +410,7 @@ class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
|
||||
final rawPhone = _phoneController.text.trim().replaceFirst('+', '');
|
||||
final success = await PhoneAuthHelper.sendOtp(rawPhone);
|
||||
if (success && mounted) {
|
||||
await PhoneAuthHelper.verifyOtp(rawPhone);
|
||||
Get.to(() => OtpVerificationScreen(phoneNumber: rawPhone));
|
||||
}
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
@@ -537,7 +537,7 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||
void _submit() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
setState(() => _isLoading = true);
|
||||
// Logic for OTP verification here
|
||||
await PhoneAuthHelper.verifyOtp(widget.phoneNumber, _otpController.text.trim());
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
@@ -596,7 +596,7 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||
controller: _otpController,
|
||||
textAlign: TextAlign.center,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 5,
|
||||
maxLength: 3,
|
||||
style: TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -605,7 +605,7 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
hintText: '·····',
|
||||
hintText: '···',
|
||||
hintStyle: TextStyle(
|
||||
color: isDark ? Colors.white12 : const Color(0xFFD1D5DB),
|
||||
letterSpacing: 20,
|
||||
@@ -615,7 +615,7 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 6),
|
||||
),
|
||||
validator: (v) => v == null || v.length < 5 ? '' : null,
|
||||
validator: (v) => v == null || v.length < 3 ? '' : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -625,10 +625,10 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||
ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _otpController,
|
||||
builder: (_, value, __) {
|
||||
final filled = value.text.length.clamp(0, 5);
|
||||
final filled = value.text.length.clamp(0, 3);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(5, (i) {
|
||||
children: List.generate(3, (i) {
|
||||
final active = i < filled;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
|
||||
@@ -23,9 +23,9 @@ class OtpVerificationPage extends StatefulWidget {
|
||||
|
||||
class _OtpVerificationPageState extends State<OtpVerificationPage> {
|
||||
late final OtpVerificationController controller;
|
||||
final List<FocusNode> _focusNodes = List.generate(6, (index) => FocusNode());
|
||||
final List<FocusNode> _focusNodes = List.generate(3, (index) => FocusNode());
|
||||
final List<TextEditingController> _textControllers =
|
||||
List.generate(5, (index) => TextEditingController());
|
||||
List.generate(3, (index) => TextEditingController());
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -50,7 +50,7 @@ class _OtpVerificationPageState extends State<OtpVerificationPage> {
|
||||
|
||||
void _onOtpChanged(String value, int index) {
|
||||
if (value.isNotEmpty) {
|
||||
if (index < 5) {
|
||||
if (index < 2) {
|
||||
_focusNodes[index + 1].requestFocus();
|
||||
} else {
|
||||
_focusNodes[index].unfocus(); // إلغاء التركيز بعد آخر حقل
|
||||
@@ -67,7 +67,7 @@ class _OtpVerificationPageState extends State<OtpVerificationPage> {
|
||||
textDirection: TextDirection.ltr, // لضمان ترتيب الحقول من اليسار لليمين
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(5, (index) {
|
||||
children: List.generate(3, (index) {
|
||||
return SizedBox(
|
||||
width: 45,
|
||||
height: 55,
|
||||
|
||||
@@ -3,8 +3,13 @@ import 'package:get/get.dart';
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../controller/functions/crud.dart';
|
||||
import '../../controller/functions/package_info.dart';
|
||||
import '../../controller/home/map_passenger_controller.dart';
|
||||
import '../../controller/home/map/map_socket_controller.dart';
|
||||
import '../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../controller/home/map/location_search_controller.dart';
|
||||
import '../../controller/home/map/nearby_drivers_controller.dart';
|
||||
import '../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../controller/home/map/ui_interactions_controller.dart';
|
||||
import '../../controller/home/map/ride_state.dart';
|
||||
import '../../main.dart';
|
||||
import '../../views/home/map_widget.dart/ride_begin_passenger.dart';
|
||||
|
||||
@@ -17,7 +22,7 @@ import 'map_widget.dart/google_map_passenger_widget.dart';
|
||||
import 'map_widget.dart/left_main_menu_icons.dart';
|
||||
import 'map_widget.dart/main_bottom_menu_map.dart';
|
||||
import 'map_widget.dart/map_menu_widget.dart';
|
||||
import 'map_widget.dart/menu_map_page.dart';
|
||||
import '../../controller/functions/package_info.dart';
|
||||
import 'map_widget.dart/passengerRideLoctionWidget.dart';
|
||||
import 'map_widget.dart/payment_method.page.dart';
|
||||
import 'map_widget.dart/points_page_for_rider.dart';
|
||||
@@ -30,9 +35,14 @@ class MapPagePassenger extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(MapPassengerController());
|
||||
Get.put(MyMenuController());
|
||||
Get.put(CRUD());
|
||||
Get.find<MapSocketController>();
|
||||
Get.find<MapEngineController>();
|
||||
Get.find<LocationSearchController>();
|
||||
Get.find<NearbyDriversController>();
|
||||
Get.find<RideLifecycleController>();
|
||||
Get.find<UiInteractionsController>();
|
||||
Get.find<MyMenuController>();
|
||||
Get.find<CRUD>();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
checkForUpdate(context);
|
||||
});
|
||||
@@ -118,7 +128,7 @@ class CancelRidePageShow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
return GetBuilder<RideLifecycleController>(
|
||||
builder: (controller) {
|
||||
// نستخدم RideState Enum لأنه أدق، أو نصلح المنطق النصي
|
||||
// الشرط:
|
||||
@@ -175,7 +185,7 @@ class PickerIconOnMap extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
return GetBuilder<RideLifecycleController>(
|
||||
builder: (controller) => controller.isPickerShown
|
||||
? Positioned(
|
||||
bottom: Get.height * .2,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:Intaleq/constant/colors.dart';
|
||||
import 'package:Intaleq/constant/links.dart';
|
||||
import 'package:Intaleq/constant/style.dart';
|
||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_state.dart';
|
||||
import 'package:Intaleq/controller/voice_call_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -10,6 +12,7 @@ import 'package:intl/intl.dart';
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../controller/firebase/notification_service.dart';
|
||||
import '../../../controller/functions/launch.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
class ApplyOrderWidget extends StatelessWidget {
|
||||
@@ -29,7 +32,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
return Obx(() {
|
||||
final controller = Get.find<MapPassengerController>();
|
||||
final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
final bool isVisible =
|
||||
controller.currentRideState.value == RideState.driverApplied ||
|
||||
@@ -57,7 +60,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
),
|
||||
// تغيير: تقليل الحواف الخارجية بشكل كبير
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: GetBuilder<MapPassengerController>(
|
||||
child: GetBuilder<RideLifecycleController>(
|
||||
builder: (c) {
|
||||
return Column(
|
||||
mainAxisSize:
|
||||
@@ -106,15 +109,18 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
// [NEW] 1. صف الرأس المضغوط (يحتوي الحالة + الإحصائيات + السعر)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildCompactHeaderRow(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
BuildContext context, RideLifecycleController controller) {
|
||||
// تنسيق السعر
|
||||
final formatter = NumberFormat("#,###");
|
||||
String formattedPrice = formatter.format(controller.totalPassenger);
|
||||
|
||||
// حساب الدقائق
|
||||
int minutes =
|
||||
(controller.timeToPassengerFromDriverAfterApplied / 60).ceil();
|
||||
if (minutes < 1) minutes = 1;
|
||||
// حساب الدقائق من الوقت المتبقي الحي، وليس ETA الأصلي فقط.
|
||||
final int secondsToPassenger =
|
||||
controller.remainingTimeToPassengerFromDriverAfterApplied > 0
|
||||
? controller.remainingTimeToPassengerFromDriverAfterApplied
|
||||
: controller.timeToPassengerFromDriverAfterApplied;
|
||||
final int minutes =
|
||||
secondsToPassenger <= 0 ? 0 : (secondsToPassenger / 60).ceil();
|
||||
|
||||
// تنسيق المسافة
|
||||
String distanceDisplay = "";
|
||||
@@ -151,7 +157,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
children: [
|
||||
_buildMiniStatChip(
|
||||
icon: Icons.access_time_filled_rounded,
|
||||
text: "$minutes ${'min'.tr}",
|
||||
text: minutes > 0 ? "$minutes ${'min'.tr}" : "--",
|
||||
color: AppColor.primaryColor,
|
||||
bgColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
),
|
||||
@@ -229,7 +235,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
// [MODIFIED] 2. كرت المعلومات المضغوط جداً
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildCompactInfoCard(BuildContext context,
|
||||
MapPassengerController controller, Color Function(String) parseColor) {
|
||||
RideLifecycleController controller, Color Function(String) parseColor) {
|
||||
return Container(
|
||||
// تقليل الحواف الداخلية للكرت
|
||||
padding: const EdgeInsets.all(10),
|
||||
@@ -312,7 +318,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildMicroCarIcon(
|
||||
MapPassengerController controller, Color Function(String) parseColor) {
|
||||
RideLifecycleController controller, Color Function(String) parseColor) {
|
||||
Color carColor = parseColor(controller.colorHex);
|
||||
return Container(
|
||||
height: 40, // تصغير من 50
|
||||
@@ -343,9 +349,8 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
color: Get.isDarkMode ? Colors.grey[850] : const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: Get.isDarkMode
|
||||
? Colors.white10
|
||||
: Colors.grey.withOpacity(0.3)),
|
||||
color:
|
||||
Get.isDarkMode ? Colors.white10 : Colors.grey.withOpacity(0.3)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
@@ -374,7 +379,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
// [MODIFIED] 3. أزرار الاتصال (Slim Buttons)
|
||||
// ---------------------------------------------------------------------------
|
||||
Widget _buildCompactButtonsRow(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
BuildContext context, RideLifecycleController controller) {
|
||||
return SizedBox(
|
||||
height: 40, // تحديد ارتفاع ثابت وصغير للأزرار
|
||||
child: Row(
|
||||
@@ -397,7 +402,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
bgColor: AppColor.greenColor,
|
||||
onTap: () {
|
||||
HapticFeedback.heavyImpact();
|
||||
makePhoneCall(controller.driverPhone);
|
||||
_showCallSelectionDialog(context, controller);
|
||||
},
|
||||
isPrimary: true,
|
||||
),
|
||||
@@ -407,6 +412,73 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _showCallSelectionDialog(
|
||||
BuildContext context, RideLifecycleController controller) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Call Options'.tr,
|
||||
style: AppStyle.title
|
||||
.copyWith(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Choose how you want to call the driver'.tr,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColor.greenColor.withOpacity(0.1),
|
||||
child: Icon(Icons.phone_android_rounded,
|
||||
color: AppColor.greenColor),
|
||||
),
|
||||
title: Text('Standard Call'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text('Uses cellular network'.tr,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
makePhoneCall(controller.driverPhone);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
child: Icon(Icons.wifi_calling_3_rounded,
|
||||
color: AppColor.primaryColor),
|
||||
),
|
||||
title: Text('Free Call'.tr,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text('Voice call over internet'.tr,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
onTap: () {
|
||||
Get.back();
|
||||
final voiceCtrl = Get.find<VoiceCallController>();
|
||||
final passengerId = box.read(BoxName.passengerID).toString();
|
||||
voiceCtrl.startCall(
|
||||
rideIdVal: controller.rideId,
|
||||
driverId: controller.driverId,
|
||||
passengerId: passengerId,
|
||||
remoteNameVal: controller.driverName,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSlimButton({
|
||||
required String label,
|
||||
required IconData icon,
|
||||
@@ -444,7 +516,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
|
||||
// --- النوافذ المنبثقة للرسائل (نفس الكود السابق مع تحسين بسيط) ---
|
||||
void _showContactOptionsDialog(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
BuildContext context, RideLifecycleController controller) {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
@@ -470,7 +542,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildPredefinedMessages(MapPassengerController controller) {
|
||||
List<Widget> _buildPredefinedMessages(RideLifecycleController controller) {
|
||||
const messages = [
|
||||
'Hello, I\'m at the agreed-upon location',
|
||||
'I\'m waiting for you',
|
||||
@@ -510,7 +582,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildCustomMessageInput(
|
||||
MapPassengerController controller, BuildContext context) {
|
||||
RideLifecycleController controller, BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -555,7 +627,22 @@ class ApplyOrderWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _sendMessage(MapPassengerController controller, String text) {
|
||||
void _sendMessage(RideLifecycleController controller, String text) async {
|
||||
try {
|
||||
await CRUD().post(
|
||||
link: AppLink.sendChatMessage,
|
||||
payload: {
|
||||
'ride_id': controller.rideId.toString(),
|
||||
'sender_id': box.read(BoxName.passengerID).toString(),
|
||||
'receiver_id': controller.driverId.toString(),
|
||||
'sender_type': 'passenger',
|
||||
'message_content': text.tr,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// Ignore or log error
|
||||
}
|
||||
|
||||
NotificationService.sendNotification(
|
||||
category: 'MSG_FROM_PASSENGER',
|
||||
target: controller.driverToken.toString(),
|
||||
@@ -577,7 +664,7 @@ class DriverArrivePassengerAndWaitMinute extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
@@ -619,7 +706,7 @@ class TimeDriverToPassenger extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
if (controller.timeToPassengerFromDriverAfterApplied <= 0) {
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ import 'package:get/get.dart';
|
||||
import 'package:Intaleq/controller/payment/payment_controller.dart';
|
||||
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
|
||||
GetBuilder<MapPassengerController> buttomSheetMapPage() {
|
||||
GetBuilder<RideLifecycleController> buttomSheetMapPage() {
|
||||
Get.put(PaymentController());
|
||||
return GetBuilder<MapPassengerController>(
|
||||
return GetBuilder<RideLifecycleController>(
|
||||
builder: (controller) =>
|
||||
controller.isBottomSheetShown && controller.rideConfirm == false
|
||||
? const Positioned(
|
||||
@@ -508,7 +508,7 @@ class Details extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
return GetBuilder<RideLifecycleController>(
|
||||
builder: (controller) => Column(
|
||||
children: [
|
||||
Row(
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:Intaleq/constant/colors.dart';
|
||||
import 'package:Intaleq/constant/style.dart';
|
||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
|
||||
// دالة لإظهار الشيت
|
||||
@@ -21,7 +21,7 @@ class CancelRidePageWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// تأكد من وجود الكنترولر
|
||||
final controller = Get.find<MapPassengerController>();
|
||||
final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
final List<String> reasons = [
|
||||
"Changed my mind".tr,
|
||||
@@ -39,7 +39,7 @@ class CancelRidePageWidget extends StatelessWidget {
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(25)),
|
||||
),
|
||||
child: GetBuilder<MapPassengerController>(
|
||||
child: GetBuilder<RideLifecycleController>(
|
||||
builder: (controller) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
|
||||
@@ -12,7 +12,7 @@ import 'dart:ui';
|
||||
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../controller/functions/tts.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../widgets/mydialoug.dart';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -62,7 +62,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
CarDetailsTypeToChoose({super.key});
|
||||
final textToSpeechController = Get.find<TextToSpeechController>();
|
||||
|
||||
void _prepareCarTypes(MapPassengerController controller) {
|
||||
void _prepareCarTypes(RideLifecycleController controller) {
|
||||
if (controller.distance > 23) {
|
||||
if (!carTypes.any((car) => car.carType == 'Rayeh Gai')) {
|
||||
carTypes.add(CarType(
|
||||
@@ -77,7 +77,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
_prepareCarTypes(controller);
|
||||
|
||||
if (!(controller.isBottomSheetShown) && controller.rideConfirm == false) {
|
||||
@@ -170,7 +170,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// HEADER
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
Widget _buildHeader(MapPassengerController controller) {
|
||||
Widget _buildHeader(RideLifecycleController controller) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(22, 4, 22, 8),
|
||||
child: Column(
|
||||
@@ -290,7 +290,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// CAR CARD
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
Widget _buildCarCard(BuildContext context, MapPassengerController controller,
|
||||
Widget _buildCarCard(BuildContext context, RideLifecycleController controller,
|
||||
CarType carType, bool isSelected, int index) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
@@ -437,7 +437,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
// PROMO BUTTON
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
Widget _buildPromoButton(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
BuildContext context, RideLifecycleController controller) {
|
||||
if (controller.promoTaken) return const SizedBox.shrink();
|
||||
|
||||
return Padding(
|
||||
@@ -511,7 +511,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// NEGATIVE BALANCE WARNING
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
Widget _buildNegativeBalanceWarning(MapPassengerController controller) {
|
||||
Widget _buildNegativeBalanceWarning(RideLifecycleController controller) {
|
||||
final passengerWallet =
|
||||
double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ?? 0.0;
|
||||
if (passengerWallet < 0.0) {
|
||||
@@ -556,7 +556,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
// PRICING HELPERS (Unchanged logic)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
String _getPassengerPriceText(
|
||||
CarType carType, MapPassengerController mapPassengerController) {
|
||||
CarType carType, RideLifecycleController mapPassengerController) {
|
||||
double rawPrice;
|
||||
switch (carType.carType) {
|
||||
case 'Comfort':
|
||||
@@ -596,7 +596,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
// DIALOGS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
void _showPromoCodeDialog(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
BuildContext context, RideLifecycleController controller) {
|
||||
Get.dialog(
|
||||
Dialog(
|
||||
shape:
|
||||
@@ -671,7 +671,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
|
||||
void _showCarDetailsDialog(
|
||||
BuildContext context,
|
||||
MapPassengerController mapPassengerController,
|
||||
RideLifecycleController mapPassengerController,
|
||||
CarType carType,
|
||||
TextToSpeechController textToSpeechController) {
|
||||
Get.dialog(
|
||||
@@ -843,7 +843,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
// LOGIC HELPERS (Unchanged)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
String _getCarDescription(
|
||||
MapPassengerController mapPassengerController, CarType carType) {
|
||||
RideLifecycleController mapPassengerController, CarType carType) {
|
||||
switch (carType.carType) {
|
||||
case 'Comfort':
|
||||
return mapPassengerController.endNameAddress
|
||||
@@ -881,7 +881,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _handleCarSelection(BuildContext context,
|
||||
MapPassengerController mapPassengerController, CarType carType) {
|
||||
RideLifecycleController mapPassengerController, CarType carType) {
|
||||
box.write(BoxName.carType, carType.carType);
|
||||
mapPassengerController.totalPassenger =
|
||||
_getOriginalPrice(carType, mapPassengerController);
|
||||
@@ -932,7 +932,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
}
|
||||
|
||||
double _getOriginalPrice(
|
||||
CarType carType, MapPassengerController mapPassengerController) {
|
||||
CarType carType, RideLifecycleController mapPassengerController) {
|
||||
switch (carType.carType) {
|
||||
case 'Comfort':
|
||||
return mapPassengerController.totalPassengerComfort;
|
||||
@@ -953,7 +953,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
|
||||
Widget _buildRayehGaiOption(
|
||||
BuildContext context,
|
||||
MapPassengerController mapPassengerController,
|
||||
RideLifecycleController mapPassengerController,
|
||||
String carTypeName,
|
||||
double price) {
|
||||
return GestureDetector(
|
||||
@@ -983,7 +983,7 @@ class BurcMoney extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
return GetBuilder<RideLifecycleController>(
|
||||
builder: (mapPassengerController) {
|
||||
final passengerWallet =
|
||||
double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ??
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:Intaleq/views/home/my_wallet/passenger_wallet.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/info.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../../controller/payment/payment_controller.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
@@ -17,7 +17,7 @@ class CashConfirmPageShown extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
// شرط الإظهار الرئيسي لم يتغير
|
||||
return Positioned(
|
||||
bottom: 0,
|
||||
|
||||
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'hexegone_clipper.dart';
|
||||
|
||||
GetBuilder<MapPassengerController> hexagonClipper() {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
GetBuilder<RideLifecycleController> hexagonClipper() {
|
||||
return GetBuilder<RideLifecycleController>(
|
||||
builder: ((controller) => controller.rideConfirm
|
||||
? Positioned(
|
||||
top: Get.height * .1,
|
||||
|
||||
@@ -4,14 +4,14 @@ import 'package:intl/intl.dart';
|
||||
// import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
|
||||
class DriverTimeArrivePassengerPage extends StatelessWidget {
|
||||
const DriverTimeArrivePassengerPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
return GetBuilder<RideLifecycleController>(
|
||||
builder: (controller) {
|
||||
return controller.remainingTime == 0
|
||||
? Positioned(
|
||||
|
||||
@@ -12,54 +12,51 @@ import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/functions/toast.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/location_search_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../../controller/home/map/ride_state.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
// ---------------------------------------------------
|
||||
// -- Widget for Destination Point Search (Optimized) --
|
||||
// ---------------------------------------------------
|
||||
|
||||
/// A more optimized and cleaner implementation of the destination search form.
|
||||
///
|
||||
/// Improvements:
|
||||
/// 1. **Widget Refactoring**: The UI is broken down into smaller, focused widgets
|
||||
/// (_SearchField, _QuickActions, _SearchResults) to prevent unnecessary rebuilds.
|
||||
/// 2. **State Management Scoping**: `GetBuilder` is used only on widgets that
|
||||
/// actually need to update, not the entire form.
|
||||
/// 3. **Reduced Build Logic**: Logic like reading from `box` is done once.
|
||||
/// 4. **Readability**: Code is cleaner and easier to follow.
|
||||
GetBuilder<MapPassengerController> formSearchPlacesDestenation() {
|
||||
// --- [تحسين] قراءة القيم مرة واحدة في بداية البناء ---
|
||||
// Store box values in local variables to avoid repeated calls inside the build method.
|
||||
GetBuilder<LocationSearchController> formSearchPlacesDestenation() {
|
||||
final String addWorkValue =
|
||||
box.read(BoxName.addWork)?.toString() ?? 'addWork';
|
||||
final String addHomeValue =
|
||||
box.read(BoxName.addHome)?.toString() ?? 'addHome';
|
||||
|
||||
// --- [ملاحظة] تأكد من أن القيم الأولية موجودة ---
|
||||
// This initialization can be moved to your app's startup logic or a splash screen controller.
|
||||
if (addWorkValue.isEmpty || addHomeValue.isEmpty) {
|
||||
box.write(BoxName.addWork, 'addWork');
|
||||
box.write(BoxName.addHome, 'addHome');
|
||||
}
|
||||
|
||||
return GetBuilder<MapPassengerController>(
|
||||
id: 'destination_form', // Use an ID to allow targeted updates
|
||||
return GetBuilder<LocationSearchController>(
|
||||
id: 'destination_form',
|
||||
builder: (controller) {
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
return Column(
|
||||
children: [
|
||||
// --- Widget for the search text field ---
|
||||
_SearchField(controller: controller),
|
||||
|
||||
// --- Widget for "Add Work" and "Add Home" buttons ---
|
||||
_SearchField(
|
||||
controller: controller,
|
||||
mapEngine: mapEngine,
|
||||
rideLifecycle: rideLifecycle,
|
||||
),
|
||||
_QuickActions(
|
||||
controller: controller,
|
||||
mapEngine: mapEngine,
|
||||
rideLifecycle: rideLifecycle,
|
||||
addWorkValue: addWorkValue,
|
||||
addHomeValue: addHomeValue,
|
||||
),
|
||||
|
||||
// --- Widget for displaying search results, wrapped in its own GetBuilder ---
|
||||
_SearchResults(),
|
||||
_SearchResults(
|
||||
controller: controller,
|
||||
mapEngine: mapEngine,
|
||||
rideLifecycle: rideLifecycle,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -70,11 +67,16 @@ GetBuilder<MapPassengerController> formSearchPlacesDestenation() {
|
||||
// -- Private Helper Widgets for Cleaner Code --
|
||||
// ---------------------------------------------------
|
||||
|
||||
/// A dedicated widget for the search input field.
|
||||
class _SearchField extends StatefulWidget {
|
||||
final MapPassengerController controller;
|
||||
final LocationSearchController controller;
|
||||
final MapEngineController mapEngine;
|
||||
final RideLifecycleController rideLifecycle;
|
||||
|
||||
const _SearchField({required this.controller});
|
||||
const _SearchField({
|
||||
required this.controller,
|
||||
required this.mapEngine,
|
||||
required this.rideLifecycle,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_SearchField> createState() => _SearchFieldState();
|
||||
@@ -83,7 +85,6 @@ class _SearchField extends StatefulWidget {
|
||||
class _SearchFieldState extends State<_SearchField> {
|
||||
Timer? _debounce;
|
||||
|
||||
// --- [إصلاح] Listener لتحديث الواجهة عند تغيير النص لإظهار/إخفاء زر المسح ---
|
||||
void _onTextChanged() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
@@ -93,20 +94,18 @@ class _SearchFieldState extends State<_SearchField> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Add listener to update the suffix icon when text changes
|
||||
widget.controller.placeDestinationController.addListener(_onTextChanged);
|
||||
}
|
||||
|
||||
// --- [تحسين] إضافة Debouncer لتأخير البحث أثناء الكتابة ---
|
||||
void _onSearchChanged(String query) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 500), () {
|
||||
if (query.length > 2) {
|
||||
widget.controller.getPlaces();
|
||||
widget.controller.changeHeightPlaces();
|
||||
widget.mapEngine.changeHeightPlaces();
|
||||
} else if (query.isEmpty) {
|
||||
widget.controller.clearPlacesDestination();
|
||||
widget.controller.changeHeightPlaces();
|
||||
widget.mapEngine.changeHeightPlaces();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -114,7 +113,6 @@ class _SearchFieldState extends State<_SearchField> {
|
||||
@override
|
||||
void dispose() {
|
||||
_debounce?.cancel();
|
||||
// Remove the listener to prevent memory leaks
|
||||
widget.controller.placeDestinationController.removeListener(_onTextChanged);
|
||||
super.dispose();
|
||||
}
|
||||
@@ -133,18 +131,15 @@ class _SearchFieldState extends State<_SearchField> {
|
||||
hintText: widget.controller.hintTextDestinationPoint,
|
||||
hintStyle: AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
||||
prefixIcon: Icon(Icons.search, color: AppColor.primaryColor),
|
||||
// --- [إصلاح] تم استبدال Obx بشرط بسيط لأن `setState` يعيد بناء الواجهة الآن ---
|
||||
suffixIcon: widget
|
||||
.controller.placeDestinationController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
||||
onPressed: () {
|
||||
widget.controller.placeDestinationController.clear();
|
||||
// The listener will automatically handle the UI update
|
||||
// And _onSearchChanged will handle clearing the results
|
||||
},
|
||||
)
|
||||
: null, // Use null instead of SizedBox for better practice
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0, vertical: 10.0),
|
||||
border: OutlineInputBorder(
|
||||
@@ -163,12 +158,12 @@ class _SearchFieldState extends State<_SearchField> {
|
||||
const SizedBox(width: 8.0),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
widget.controller.changeMainBottomMenuMap();
|
||||
widget.controller.changePickerShown();
|
||||
widget.mapEngine.changeMainBottomMenuMap();
|
||||
widget.mapEngine.changePickerShown();
|
||||
},
|
||||
icon: Icon(Icons.location_on_outlined,
|
||||
color: AppColor.accentColor, size: 30),
|
||||
tooltip: widget.controller.isAnotherOreder
|
||||
tooltip: widget.rideLifecycle.isAnotherOreder
|
||||
? 'Pick destination on map'.tr
|
||||
: 'Pick on map'.tr,
|
||||
),
|
||||
@@ -178,14 +173,17 @@ class _SearchFieldState extends State<_SearchField> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A dedicated widget for the quick action buttons (Work/Home).
|
||||
class _QuickActions extends StatelessWidget {
|
||||
final MapPassengerController controller;
|
||||
final LocationSearchController controller;
|
||||
final MapEngineController mapEngine;
|
||||
final RideLifecycleController rideLifecycle;
|
||||
final String addWorkValue;
|
||||
final String addHomeValue;
|
||||
|
||||
const _QuickActions({
|
||||
required this.controller,
|
||||
required this.mapEngine,
|
||||
required this.rideLifecycle,
|
||||
required this.addWorkValue,
|
||||
required this.addHomeValue,
|
||||
});
|
||||
@@ -203,13 +201,20 @@ class _QuickActions extends StatelessWidget {
|
||||
onTap: () {
|
||||
if (addWorkValue == 'addWork') {
|
||||
controller.workLocationFromMap = true;
|
||||
controller.changeMainBottomMenuMap();
|
||||
controller.changePickerShown();
|
||||
mapEngine.changeMainBottomMenuMap();
|
||||
mapEngine.changePickerShown();
|
||||
} else {
|
||||
_handleQuickAction(controller, BoxName.addWork, 'To Work');
|
||||
_handleQuickAction(
|
||||
controller,
|
||||
mapEngine,
|
||||
rideLifecycle,
|
||||
BoxName.addWork,
|
||||
'To Work',
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () => _showChangeLocationDialog(controller, 'Work'),
|
||||
onLongPress: () =>
|
||||
_showChangeLocationDialog(controller, mapEngine, 'Work'),
|
||||
),
|
||||
_buildQuickActionButton(
|
||||
icon: Icons.home_outlined,
|
||||
@@ -217,13 +222,20 @@ class _QuickActions extends StatelessWidget {
|
||||
onTap: () {
|
||||
if (addHomeValue == 'addHome') {
|
||||
controller.homeLocationFromMap = true;
|
||||
controller.changeMainBottomMenuMap();
|
||||
controller.changePickerShown();
|
||||
mapEngine.changeMainBottomMenuMap();
|
||||
mapEngine.changePickerShown();
|
||||
} else {
|
||||
_handleQuickAction(controller, BoxName.addHome, 'To Home');
|
||||
_handleQuickAction(
|
||||
controller,
|
||||
mapEngine,
|
||||
rideLifecycle,
|
||||
BoxName.addHome,
|
||||
'To Home',
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () => _showChangeLocationDialog(controller, 'Home'),
|
||||
onLongPress: () =>
|
||||
_showChangeLocationDialog(controller, mapEngine, 'Home'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -231,17 +243,25 @@ class _QuickActions extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// A dedicated widget for the search results list.
|
||||
/// It uses its own `GetBuilder` to only rebuild when the list of places changes.
|
||||
class _SearchResults extends StatelessWidget {
|
||||
final LocationSearchController controller;
|
||||
final MapEngineController mapEngine;
|
||||
final RideLifecycleController rideLifecycle;
|
||||
|
||||
const _SearchResults({
|
||||
required this.controller,
|
||||
required this.mapEngine,
|
||||
required this.rideLifecycle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
id: 'places_list', // Use a specific ID for targeted updates
|
||||
builder: (controller) {
|
||||
return GetBuilder<LocationSearchController>(
|
||||
id: 'places_list',
|
||||
builder: (locCtrl) {
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: controller.placesDestination.isNotEmpty ? 300 : 0,
|
||||
height: locCtrl.placesDestination.isNotEmpty ? 300 : 0,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
@@ -250,11 +270,11 @@ class _SearchResults extends StatelessWidget {
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
itemCount: controller.placesDestination.length,
|
||||
itemCount: locCtrl.placesDestination.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const Divider(height: 1, color: Colors.grey),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final res = controller.placesDestination[index];
|
||||
final res = locCtrl.placesDestination[index];
|
||||
final title = res['name_ar'] ?? res['name'] ?? 'Unknown Place';
|
||||
final address = res['address'] ?? 'Details not available';
|
||||
final latitude = res['latitude'];
|
||||
@@ -277,7 +297,14 @@ class _SearchResults extends StatelessWidget {
|
||||
context, latitude, longitude, title),
|
||||
),
|
||||
onTap: () => _handlePlaceSelection(
|
||||
controller, latitude, longitude, title, index),
|
||||
controller,
|
||||
mapEngine,
|
||||
rideLifecycle,
|
||||
latitude,
|
||||
longitude,
|
||||
title,
|
||||
index,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -286,7 +313,6 @@ class _SearchResults extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// --- [تحسين] استخراج المنطق المعقد إلى دوال مساعدة ---
|
||||
Future<void> _handleAddToFavorites(BuildContext context, dynamic latitude,
|
||||
dynamic longitude, String title) async {
|
||||
if (latitude != null && longitude != null) {
|
||||
@@ -311,14 +337,19 @@ class _SearchResults extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handlePlaceSelection(MapPassengerController controller,
|
||||
dynamic latitude, dynamic longitude, String title, int index) async {
|
||||
Future<void> _handlePlaceSelection(
|
||||
LocationSearchController controller,
|
||||
MapEngineController mapEngine,
|
||||
RideLifecycleController rideLifecycle,
|
||||
dynamic latitude,
|
||||
dynamic longitude,
|
||||
String title,
|
||||
int index) async {
|
||||
if (latitude == null || longitude == null) {
|
||||
Toast.show(Get.context!, 'Invalid location data', AppColor.redColor);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save to recent locations
|
||||
await sql.insertMapLocation({
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
@@ -330,53 +361,55 @@ class _SearchResults extends StatelessWidget {
|
||||
final destLatLng = LatLng(
|
||||
double.parse(latitude.toString()), double.parse(longitude.toString()));
|
||||
|
||||
if (controller.isAnotherOreder) {
|
||||
// **Another Order Flow**
|
||||
await _handleAnotherOrderSelection(controller, destLatLng);
|
||||
if (rideLifecycle.isAnotherOreder) {
|
||||
await _handleAnotherOrderSelection(
|
||||
controller, mapEngine, rideLifecycle, destLatLng);
|
||||
} else {
|
||||
// **Regular Order Flow**
|
||||
_handleRegularOrderSelection(controller, destLatLng, index);
|
||||
_handleRegularOrderSelection(
|
||||
controller, mapEngine, rideLifecycle, destLatLng, index);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleAnotherOrderSelection(
|
||||
MapPassengerController controller, LatLng destination) async {
|
||||
LocationSearchController controller,
|
||||
MapEngineController mapEngine,
|
||||
RideLifecycleController rideLifecycle,
|
||||
LatLng destination) async {
|
||||
controller.myDestination = destination;
|
||||
controller.clearPlacesDestination(); // Helper method in controller
|
||||
controller.clearPlacesDestination();
|
||||
|
||||
await controller.getDirectionMap(
|
||||
await rideLifecycle.getDirectionMap(
|
||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
||||
'${controller.myDestination.latitude},${controller.myDestination.longitude}');
|
||||
|
||||
controller.isPickerShown = false;
|
||||
mapEngine.isPickerShown = false;
|
||||
controller.passengerStartLocationFromMap = false;
|
||||
controller.changeMainBottomMenuMap();
|
||||
controller.showBottomSheet1();
|
||||
mapEngine.changeMainBottomMenuMap();
|
||||
rideLifecycle.showBottomSheet1();
|
||||
}
|
||||
|
||||
void _handleRegularOrderSelection(
|
||||
MapPassengerController controller, LatLng destination, int index) {
|
||||
LocationSearchController controller,
|
||||
MapEngineController mapEngine,
|
||||
RideLifecycleController rideLifecycle,
|
||||
LatLng destination,
|
||||
int index) {
|
||||
controller.passengerLocation = controller.newMyLocation;
|
||||
controller.myDestination = destination;
|
||||
controller.convertHintTextDestinationNewPlaces(index);
|
||||
|
||||
controller.clearPlacesDestination(); // Helper method in controller
|
||||
controller.clearPlacesDestination();
|
||||
|
||||
controller.changeMainBottomMenuMap();
|
||||
mapEngine.changeMainBottomMenuMap();
|
||||
controller.passengerStartLocationFromMap = true;
|
||||
controller.isPickerShown = true;
|
||||
mapEngine.isPickerShown = true;
|
||||
|
||||
// ✅ FIX: Draw the route after setting destination (matching the "Another Order" flow)
|
||||
controller.getDirectionMap(
|
||||
rideLifecycle.getDirectionMap(
|
||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
||||
'${controller.myDestination.latitude},${controller.myDestination.longitude}');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------
|
||||
// -- Helper Functions (kept from original code) --
|
||||
// ---------------------------------------------------
|
||||
|
||||
Widget _buildQuickActionButton({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
@@ -410,10 +443,12 @@ Widget _buildQuickActionButton({
|
||||
);
|
||||
}
|
||||
|
||||
void _showChangeLocationDialog(
|
||||
MapPassengerController controller, String locationType) {
|
||||
void _showChangeLocationDialog(LocationSearchController controller,
|
||||
MapEngineController mapEngine, String locationType) {
|
||||
MyDialog().getDialog(
|
||||
locationType == 'Work' ? 'Change Work location ?'.tr : 'Change Home location ?'.tr,
|
||||
locationType == 'Work'
|
||||
? 'Change Work location ?'.tr
|
||||
: 'Change Home location ?'.tr,
|
||||
'',
|
||||
() {
|
||||
if (locationType == 'Work') {
|
||||
@@ -421,15 +456,18 @@ void _showChangeLocationDialog(
|
||||
} else {
|
||||
controller.homeLocationFromMap = true;
|
||||
}
|
||||
controller.changeMainBottomMenuMap();
|
||||
controller.changePickerShown();
|
||||
mapEngine.changeMainBottomMenuMap();
|
||||
mapEngine.changePickerShown();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _handleQuickAction(
|
||||
MapPassengerController controller, String boxName, String hintText) async {
|
||||
// --- [تحسين] قراءة وتحويل الإحداثيات بأمان أكبر ---
|
||||
LocationSearchController controller,
|
||||
MapEngineController mapEngine,
|
||||
RideLifecycleController rideLifecycle,
|
||||
String boxName,
|
||||
String hintText) async {
|
||||
try {
|
||||
final locationString = box.read(boxName).toString();
|
||||
final parts = locationString.split(',');
|
||||
@@ -439,20 +477,19 @@ void _handleQuickAction(
|
||||
);
|
||||
|
||||
controller.hintTextDestinationPoint = hintText;
|
||||
controller.changeMainBottomMenuMap();
|
||||
mapEngine.changeMainBottomMenuMap();
|
||||
|
||||
await controller.getDirectionMap(
|
||||
await rideLifecycle.getDirectionMap(
|
||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
||||
'${latLng.latitude},${latLng.longitude}',
|
||||
);
|
||||
|
||||
controller.currentLocationToFormPlaces = false;
|
||||
controller.clearPlacesDestination(); // Helper method in controller
|
||||
controller.clearPlacesDestination();
|
||||
controller.passengerStartLocationFromMap = false;
|
||||
controller.isPickerShown = false;
|
||||
controller.showBottomSheet1();
|
||||
mapEngine.isPickerShown = false;
|
||||
rideLifecycle.showBottomSheet1();
|
||||
} catch (e) {
|
||||
// Handle error if parsing fails
|
||||
Log.print("Error handling quick action: $e");
|
||||
Toast.show(Get.context!, "Failed to get location".tr, AppColor.redColor);
|
||||
}
|
||||
|
||||
@@ -4,135 +4,122 @@ import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/location_search_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
|
||||
// ---------------------------------------------------
|
||||
// -- Widget for Start Point Search (Updated) --
|
||||
// ---------------------------------------------------
|
||||
|
||||
GetBuilder<MapPassengerController> formSearchPlacesStart() {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
id: 'start_point_form', // إضافة معرف لتحديث هذا الجزء فقط عند الحاجة
|
||||
builder: (controller) => Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// --- حقل البحث النصي ---
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller.placeStartController,
|
||||
onChanged: (value) {
|
||||
if (controller.placeStartController.text.length > 2) {
|
||||
controller.getPlacesStart();
|
||||
} else if (controller.placeStartController.text.isEmpty) {
|
||||
controller.clearPlacesStart();
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search for a starting point'.tr,
|
||||
hintStyle:
|
||||
AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
||||
prefixIcon:
|
||||
Icon(Icons.search, color: AppColor.primaryColor),
|
||||
suffixIcon: controller.placeStartController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
||||
onPressed: () {
|
||||
controller.placeStartController.clear();
|
||||
controller.clearPlacesStart();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0, vertical: 10.0),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide.none,
|
||||
GetBuilder<LocationSearchController> formSearchPlacesStart() {
|
||||
return GetBuilder<LocationSearchController>(
|
||||
id: 'start_point_form',
|
||||
builder: (controller) {
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller.placeStartController,
|
||||
onChanged: (value) {
|
||||
if (controller.placeStartController.text.length > 2) {
|
||||
controller.getPlacesStart();
|
||||
} else if (controller.placeStartController.text.isEmpty) {
|
||||
controller.clearPlacesStart();
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search for a starting point'.tr,
|
||||
hintStyle:
|
||||
AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
||||
prefixIcon:
|
||||
Icon(Icons.search, color: AppColor.primaryColor),
|
||||
suffixIcon: controller.placeStartController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
||||
onPressed: () {
|
||||
controller.placeStartController.clear();
|
||||
controller.clearPlacesStart();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0, vertical: 10.0),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide(color: AppColor.primaryColor),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[50],
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide(color: AppColor.primaryColor),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[50],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8.0),
|
||||
|
||||
// --- أيقونة اختيار الموقع من الخريطة (الجزء المضاف) ---
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// هذا السطر مهم جداً: نخبر الكونترولر أننا نحدد نقطة البداية الآن
|
||||
controller.passengerStartLocationFromMap = true;
|
||||
|
||||
// إخفاء القائمة السفلية وفتح مؤشر الخريطة (Picker)
|
||||
controller.changeMainBottomMenuMap();
|
||||
controller.changePickerShown();
|
||||
},
|
||||
icon: Icon(Icons.location_on_outlined,
|
||||
color: AppColor.accentColor, size: 30),
|
||||
tooltip: 'Pick start point on map'.tr,
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 8.0),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
controller.passengerStartLocationFromMap = true;
|
||||
mapEngine.changeMainBottomMenuMap();
|
||||
mapEngine.changePickerShown();
|
||||
},
|
||||
icon: Icon(Icons.location_on_outlined,
|
||||
color: AppColor.accentColor, size: 30),
|
||||
tooltip: 'Pick start point on map'.tr,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: controller.placesStart.isNotEmpty ? 300 : 0,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
itemCount: controller.placesStart.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const Divider(height: 1, color: Colors.grey),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var res = controller.placesStart[index];
|
||||
var title = res['name_ar'] ?? res['name'] ?? 'Unknown Place';
|
||||
var address = res['address'] ?? 'Details not available';
|
||||
|
||||
// --- قائمة نتائج البحث ---
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: controller.placesStart.isNotEmpty ? 300 : 0,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.place, size: 30, color: Colors.grey),
|
||||
title: Text(title,
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(fontWeight: FontWeight.w500)),
|
||||
subtitle: Text(address,
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 12)),
|
||||
onTap: () {
|
||||
var latitude = res['latitude'];
|
||||
var longitude = res['longitude'];
|
||||
if (latitude != null && longitude != null) {
|
||||
controller.passengerLocation =
|
||||
LatLng(double.parse(latitude), double.parse(longitude));
|
||||
controller.placeStartController.text = title;
|
||||
controller.clearPlacesStart();
|
||||
mapEngine.changeMainBottomMenuMap();
|
||||
controller.update();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const ClampingScrollPhysics(),
|
||||
itemCount: controller.placesStart.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const Divider(height: 1, color: Colors.grey),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var res = controller.placesStart[index];
|
||||
var title = res['name_ar'] ?? res['name'] ?? 'Unknown Place';
|
||||
var address = res['address'] ?? 'Details not available';
|
||||
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.place, size: 30, color: Colors.grey),
|
||||
title: Text(title,
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(fontWeight: FontWeight.w500)),
|
||||
subtitle: Text(address,
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 12)),
|
||||
onTap: () {
|
||||
var latitude = res['latitude'];
|
||||
var longitude = res['longitude'];
|
||||
if (latitude != null && longitude != null) {
|
||||
// تحديث موقع الراكب (نقطة الانطلاق) بناءً على الاختيار
|
||||
controller.passengerLocation =
|
||||
LatLng(double.parse(latitude), double.parse(longitude));
|
||||
|
||||
// تحديث النص في الحقل
|
||||
controller.placeStartController.text = title;
|
||||
|
||||
// مسح النتائج
|
||||
controller.clearPlacesStart();
|
||||
|
||||
// إغلاق القائمة والعودة للخريطة لرؤية النتيجة (اختياري حسب منطق تطبيقك)
|
||||
controller.changeMainBottomMenuMap();
|
||||
|
||||
controller.update();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,183 +6,181 @@ import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/functions/toast.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/location_search_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
GetBuilder<MapPassengerController> formSearchPlaces(int index) {
|
||||
// DbSql sql = DbSql.instance;
|
||||
return GetBuilder<MapPassengerController>(
|
||||
builder: (controller) => Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: AppColor.secondaryColor),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.only(),
|
||||
gapPadding: 4,
|
||||
borderSide: BorderSide(
|
||||
color: AppColor.redColor,
|
||||
width: 2,
|
||||
)),
|
||||
suffixIcon: const Icon(Icons.search),
|
||||
hintText: controller.hintTextwayPoint0.tr,
|
||||
hintStyle: AppStyle.title,
|
||||
hintMaxLines: 1,
|
||||
prefixIcon: IconButton(
|
||||
onPressed: () {
|
||||
controller.allTextEditingPlaces[index].clear();
|
||||
controller.clearPlaces(index);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
color: Colors.red[300],
|
||||
),
|
||||
),
|
||||
),
|
||||
controller: controller.allTextEditingPlaces[index],
|
||||
onChanged: (value) {
|
||||
if (controller.allTextEditingPlaces[index].text.length >
|
||||
5) {
|
||||
controller.getPlacesListsWayPoint(index);
|
||||
controller.changeHeightPlacesAll(index);
|
||||
}
|
||||
GetBuilder<LocationSearchController> formSearchPlaces(int index) {
|
||||
return GetBuilder<LocationSearchController>(
|
||||
builder: (controller) {
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: AppColor.secondaryColor),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.only(),
|
||||
gapPadding: 4,
|
||||
borderSide: BorderSide(
|
||||
color: AppColor.redColor,
|
||||
width: 2,
|
||||
)),
|
||||
suffixIcon: const Icon(Icons.search),
|
||||
hintText: controller.hintTextwayPoint0.tr,
|
||||
hintStyle: AppStyle.title,
|
||||
hintMaxLines: 1,
|
||||
prefixIcon: IconButton(
|
||||
onPressed: () {
|
||||
controller.allTextEditingPlaces[index].clear();
|
||||
controller.clearPlaces(index);
|
||||
},
|
||||
// onEditingComplete: () => controller.changeHeight(),
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
color: Colors.red[300],
|
||||
),
|
||||
),
|
||||
),
|
||||
controller: controller.allTextEditingPlaces[index],
|
||||
onChanged: (value) {
|
||||
if (controller.allTextEditingPlaces[index].text.length > 5) {
|
||||
controller.getPlacesListsWayPoint(index);
|
||||
mapEngine.changeHeightPlacesAll(index);
|
||||
}
|
||||
},
|
||||
),
|
||||
controller.placeListResponseAll[index].isEmpty
|
||||
? InkWell(
|
||||
onTap: () {
|
||||
controller.startLocationFromMapAll[index] = true;
|
||||
controller.wayPointIndex = index;
|
||||
Get.back();
|
||||
// controller.changeMainBottomMenuMap();
|
||||
controller.changeWayPointStopsSheet();
|
||||
controller.changePickerShown();
|
||||
},
|
||||
child: Text(
|
||||
'Choose from Map'.tr + ' $index'.tr,
|
||||
style:
|
||||
AppStyle.title.copyWith(color: AppColor.blueColor),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
Container(
|
||||
height: controller.placeListResponseAll[index].isNotEmpty
|
||||
? controller.height
|
||||
: 0,
|
||||
color: AppColor.secondaryColor,
|
||||
child: ListView.builder(
|
||||
itemCount: controller.placeListResponseAll[index].length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
var res = controller.placeListResponseAll[index][i];
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
// ── Extract selected location ──
|
||||
final double lat = res['geometry']['location']['lat'];
|
||||
final double lng = res['geometry']['location']['lng'];
|
||||
final String placeName = res['name'].toString();
|
||||
final selectedLatLng = LatLng(lat, lng);
|
||||
),
|
||||
),
|
||||
controller.placeListResponseAll[index].isEmpty
|
||||
? InkWell(
|
||||
onTap: () {
|
||||
controller.startLocationFromMapAll[index] = true;
|
||||
controller.wayPointIndex = index;
|
||||
Get.back();
|
||||
mapEngine.changeWayPointStopsSheet();
|
||||
mapEngine.changePickerShown();
|
||||
},
|
||||
child: Text(
|
||||
'Choose from Map'.tr + ' $index'.tr,
|
||||
style:
|
||||
AppStyle.title.copyWith(color: AppColor.blueColor),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
Container(
|
||||
height: controller.placeListResponseAll[index].isNotEmpty
|
||||
? mapEngine.height
|
||||
: 0,
|
||||
color: AppColor.secondaryColor,
|
||||
child: ListView.builder(
|
||||
itemCount: controller.placeListResponseAll[index].length,
|
||||
itemBuilder: (BuildContext context, int i) {
|
||||
var res = controller.placeListResponseAll[index][i];
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
final double lat = res['geometry']['location']['lat'];
|
||||
final double lng = res['geometry']['location']['lng'];
|
||||
final String placeName = res['name'].toString();
|
||||
final selectedLatLng = LatLng(lat, lng);
|
||||
|
||||
controller.changeHeightPlaces();
|
||||
mapEngine.changeHeightPlaces();
|
||||
|
||||
// ── Update start/end based on context ──
|
||||
if (controller.currentLocationToFormPlacesAll[index] ==
|
||||
true) {
|
||||
controller.newStartPointLocation =
|
||||
controller.passengerLocation;
|
||||
} else {
|
||||
controller.passengerLocation =
|
||||
controller.newStartPointLocation;
|
||||
}
|
||||
if (controller.currentLocationToFormPlacesAll[index] ==
|
||||
true) {
|
||||
controller.newStartPointLocation =
|
||||
rideLifecycle.passengerLocation;
|
||||
} else {
|
||||
rideLifecycle.passengerLocation =
|
||||
controller.newStartPointLocation;
|
||||
}
|
||||
|
||||
// ✅ FIX: Set the waypoint to the selected location
|
||||
controller.menuWaypoints[index] = selectedLatLng;
|
||||
controller.menuWaypointNames[index] = placeName;
|
||||
controller.menuWaypoints[index] = selectedLatLng;
|
||||
controller.menuWaypointNames[index] = placeName;
|
||||
|
||||
// ✅ FIX: Update hint text and coordinates
|
||||
controller.convertHintTextPlaces(index, res);
|
||||
controller.convertHintTextPlaces(index, res);
|
||||
|
||||
// ✅ FIX: Draw the route with the updated waypoint
|
||||
final String start =
|
||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}';
|
||||
final String dest =
|
||||
'${controller.myDestination.latitude},${controller.myDestination.longitude}';
|
||||
final String start =
|
||||
'${rideLifecycle.passengerLocation.latitude},${rideLifecycle.passengerLocation.longitude}';
|
||||
final String dest =
|
||||
'${rideLifecycle.myDestination.latitude},${rideLifecycle.myDestination.longitude}';
|
||||
|
||||
await controller.getDirectionMap(start, dest);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Column(
|
||||
await rideLifecycle.getDirectionMap(start, dest);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
Column(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Image.network(
|
||||
res['icon'],
|
||||
width: 20,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await sql.insertMapLocation({
|
||||
'latitude': res['geometry']
|
||||
['location']['lat'],
|
||||
'longitude': res['geometry']
|
||||
['location']['lng'],
|
||||
'name': res['name'].toString(),
|
||||
'rate': res['rating'].toString(),
|
||||
}, TableName.placesFavorite);
|
||||
Toast.show(
|
||||
context,
|
||||
'${res['name']} ${'Saved Sucssefully'.tr}',
|
||||
AppColor.primaryColor);
|
||||
},
|
||||
icon: const Icon(Icons.favorite_border),
|
||||
),
|
||||
],
|
||||
Image.network(
|
||||
res['icon'],
|
||||
width: 20,
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
res['name'].toString(),
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
res['vicinity'].toString(),
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
'rate',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
res['rating'].toString(),
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await sql.insertMapLocation({
|
||||
'latitude': res['geometry']
|
||||
['location']['lat'],
|
||||
'longitude': res['geometry']
|
||||
['location']['lng'],
|
||||
'name': res['name'].toString(),
|
||||
'rate': res['rating'].toString(),
|
||||
}, TableName.placesFavorite);
|
||||
Toast.show(
|
||||
context,
|
||||
'${res['name']} ${'Saved Sucssefully'.tr}',
|
||||
AppColor.primaryColor);
|
||||
},
|
||||
icon: const Icon(Icons.favorite_border),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
res['name'].toString(),
|
||||
style: AppStyle.title,
|
||||
),
|
||||
Text(
|
||||
res['vicinity'].toString(),
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
'rate',
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
Text(
|
||||
res['rating'].toString(),
|
||||
style: AppStyle.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
thickness: 1,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
));
|
||||
const Divider(
|
||||
thickness: 1,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,19 +6,26 @@ import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'package:Intaleq/controller/home/points_for_rider_controller.dart';
|
||||
import 'package:Intaleq/services/offline_map_service.dart';
|
||||
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/location_search_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../../controller/home/map/nearby_drivers_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../widgets/mycircular.dart';
|
||||
import '../../widgets/mydialoug.dart';
|
||||
|
||||
class GoogleMapPassengerWidget extends StatelessWidget {
|
||||
GoogleMapPassengerWidget({super.key});
|
||||
|
||||
final WayPointController wayPointController = Get.put(WayPointController());
|
||||
final WayPointController wayPointController = Get.find<WayPointController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
builder: (controller) => controller.isLoading
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
final nearbyDrivers = Get.find<NearbyDriversController>();
|
||||
|
||||
return GetBuilder<MapEngineController>(
|
||||
builder: (controller) => rideLifecycle.isLoading
|
||||
? const MyCircularProgressIndicator()
|
||||
: Positioned(
|
||||
bottom: Get.height * .2,
|
||||
@@ -32,13 +39,13 @@ class GoogleMapPassengerWidget extends StatelessWidget {
|
||||
: 'assets/style.json',
|
||||
onMapCreated: controller.onMapCreated,
|
||||
onStyleLoaded: controller.onStyleLoaded,
|
||||
onCameraMove: controller.onCameraMoveThrottled,
|
||||
onCameraMove: locationSearch.onCameraMoveThrottled,
|
||||
onCameraIdle: () {
|
||||
if (controller.mapController != null) {
|
||||
final position = controller.mapController!.cameraPosition;
|
||||
if (position != null) {
|
||||
Log.print('✅ onCameraIdle targeted: ${position.target}');
|
||||
controller
|
||||
locationSearch
|
||||
.updateCurrentLocationFromCamera(position.target);
|
||||
OfflineMapService.instance
|
||||
.downloadRegion(position.target, radiusKm: 1.0);
|
||||
@@ -54,8 +61,8 @@ class GoogleMapPassengerWidget extends StatelessWidget {
|
||||
polygons: controller.polygons,
|
||||
circles: controller.circles,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: controller.passengerLocation,
|
||||
zoom: controller.lowPerf ? 14.5 : 15,
|
||||
target: locationSearch.passengerLocation,
|
||||
zoom: nearbyDrivers.lowPerf ? 14.5 : 15,
|
||||
),
|
||||
myLocationEnabled: true,
|
||||
onTap: (latlng) => controller.hidePlaces(),
|
||||
@@ -63,11 +70,11 @@ class GoogleMapPassengerWidget extends StatelessWidget {
|
||||
MyDialog().getDialog('Are you want to go to this site'.tr, '',
|
||||
() async {
|
||||
controller.clearPolyline();
|
||||
controller.getDirectionMap(
|
||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
||||
rideLifecycle.getDirectionMap(
|
||||
'${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}',
|
||||
'${latlng.latitude},${latlng.longitude}',
|
||||
);
|
||||
controller.showBottomSheet1();
|
||||
rideLifecycle.showBottomSheet1();
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,95 +1,82 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
||||
import 'package:Intaleq/views/widgets/error_snakbar.dart';
|
||||
import 'package:Intaleq/views/widgets/mycircular.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||
import 'dart:ui'; // مهم لإضافة تأثير الضبابية
|
||||
import 'dart:ui';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/functions/tts.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/location_search_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../../controller/home/vip_waitting_page.dart';
|
||||
import '../navigation/navigation_view.dart';
|
||||
|
||||
// --- الدالة الرئيسية بالتصميم الجديد ---
|
||||
GetBuilder<MapPassengerController> leftMainMenuIcons() {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
builder: (controller) => Positioned(
|
||||
// تم تعديل الموضع ليتناسب مع التصميم الجديد
|
||||
top: Get.height * .01,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50.0), // لإنشاء شكل الكبسولة
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 8.0, sigmaY: 8.0), // تأثير الزجاج المصنفر
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor.withOpacity(0.4), // لون شبه شفاف
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
border: Border.all(color: AppColor.secondaryColor),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min, // ليأخذ الشريط حجم الأزرار فقط
|
||||
children: [
|
||||
// --- تم استخدام دالة مساعدة جديدة للزر ---
|
||||
_buildMapActionButton(
|
||||
icon: Icons.near_me_outlined,
|
||||
tooltip: 'Toggle Map Type',
|
||||
onPressed: () => Get.to(() => NavigationView()),
|
||||
),
|
||||
// _buildVerticalDivider(),
|
||||
// _buildMapActionButton(
|
||||
// icon: Icons.traffic_outlined,
|
||||
// tooltip: 'Toggle Traffic',
|
||||
// onPressed: () => controller.changeMapTraffic(),
|
||||
// ),
|
||||
_buildVerticalDivider(),
|
||||
_buildMapActionButton(
|
||||
icon: Icons.my_location_rounded,
|
||||
tooltip: 'Go to My Location',
|
||||
onPressed: () {
|
||||
controller.mapController?.animateCamera(
|
||||
CameraUpdate.newLatLng(
|
||||
LatLng(
|
||||
controller.passengerLocation.latitude,
|
||||
controller.passengerLocation.longitude,
|
||||
GetBuilder<MapEngineController> leftMainMenuIcons() {
|
||||
return GetBuilder<MapEngineController>(
|
||||
builder: (controller) {
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
return Positioned(
|
||||
top: Get.height * .01,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
border: Border.all(color: AppColor.secondaryColor),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildMapActionButton(
|
||||
icon: Icons.near_me_outlined,
|
||||
tooltip: 'Toggle Map Type',
|
||||
onPressed: () => Get.to(() => NavigationView()),
|
||||
),
|
||||
_buildVerticalDivider(),
|
||||
_buildMapActionButton(
|
||||
icon: Icons.my_location_rounded,
|
||||
tooltip: 'Go to My Location',
|
||||
onPressed: () {
|
||||
controller.mapController?.animateCamera(
|
||||
CameraUpdate.newLatLng(
|
||||
LatLng(
|
||||
locationSearch.passengerLocation.latitude,
|
||||
locationSearch.passengerLocation.longitude,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildVerticalDivider(),
|
||||
_buildMapActionButton(
|
||||
icon: Octicons.watch,
|
||||
tooltip: 'VIP Waiting Page',
|
||||
onPressed: () => Get.to(() => VipWaittingPage()),
|
||||
),
|
||||
// _buildMapActionButton(
|
||||
// icon: Octicons.ellipsis,
|
||||
// tooltip: 'test',
|
||||
// onPressed: () => Get.to(() => TestPage()),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildVerticalDivider(),
|
||||
_buildMapActionButton(
|
||||
icon: Octicons.watch,
|
||||
tooltip: 'VIP Waiting Page',
|
||||
onPressed: () => Get.to(() => VipWaittingPage()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// --- دالة مساعدة جديدة لإنشاء الأزرار بشكل أنيق ---
|
||||
Widget _buildMapActionButton({
|
||||
required IconData icon,
|
||||
required String tooltip,
|
||||
@@ -101,28 +88,23 @@ Widget _buildMapActionButton({
|
||||
tooltip: tooltip,
|
||||
splashRadius: 22,
|
||||
padding: const EdgeInsets.all(12),
|
||||
constraints: const BoxConstraints(), // لإزالة المساحات الافتراضية
|
||||
constraints: const BoxConstraints(),
|
||||
);
|
||||
}
|
||||
|
||||
// --- ويدجت للفاصل الرأسي بين الأزرار ---
|
||||
Widget _buildVerticalDivider() {
|
||||
return Container(
|
||||
height: 20,
|
||||
width: 1,
|
||||
color: AppColor.writeColor.withOpacity(0.2),
|
||||
color: AppColor.writeColor.withValues(alpha: 0.2),
|
||||
);
|
||||
}
|
||||
|
||||
// --- باقي الكود الخاص بك يبقى كما هو بدون تغيير ---
|
||||
|
||||
class TestPage extends StatelessWidget {
|
||||
const TestPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final random = Random();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('iOS Live Activity Test'),
|
||||
@@ -137,7 +119,6 @@ class TestPage extends StatelessWidget {
|
||||
title: 'title',
|
||||
onPressed: () {},
|
||||
),
|
||||
// زر الإنهاء
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../notification/notification_page.dart';
|
||||
import '../HomePage/contact_us.dart';
|
||||
import '../HomePage/share_app_page.dart';
|
||||
@@ -28,9 +28,8 @@ Color get _kBg =>
|
||||
Get.isDarkMode ? const Color(0xFF060B18) : AppColor.secondaryColor;
|
||||
Color get _kBgSurface => Get.isDarkMode
|
||||
? const Color(0xFF0D1525)
|
||||
: AppColor.secondaryColor.withOpacity(0.9);
|
||||
: AppColor.secondaryColor.withValues(alpha: 0.9);
|
||||
const _kAmber = Color(0xFFFFB700);
|
||||
Color get _kBorder => _kCyan.withOpacity(0.15);
|
||||
Color get _kText => AppColor.writeColor;
|
||||
Color get _kTextMuted => AppColor.grayColor;
|
||||
|
||||
@@ -39,16 +38,14 @@ class MapMenuWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.lazyPut(() => MapPassengerController());
|
||||
|
||||
return GetBuilder<MapPassengerController>(
|
||||
return GetBuilder<MapEngineController>(
|
||||
builder: (controller) => Stack(
|
||||
children: [
|
||||
// ── تعتيم الخلفية ───────────────────────────────────────────────
|
||||
if (controller.widthMenu > 0)
|
||||
GestureDetector(
|
||||
onTap: controller.getDrawerMenu,
|
||||
child: Container(color: Colors.black.withOpacity(0.55)),
|
||||
child: Container(color: Colors.black.withValues(alpha: 0.55)),
|
||||
),
|
||||
|
||||
_buildSideMenu(controller),
|
||||
@@ -59,7 +56,7 @@ class MapMenuWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
// ── زر القائمة العائم ────────────────────────────────────────────────────
|
||||
Widget _buildMenuButton(MapPassengerController controller) {
|
||||
Widget _buildMenuButton(MapEngineController controller) {
|
||||
return Positioned(
|
||||
top: 45,
|
||||
left: 16,
|
||||
@@ -76,12 +73,12 @@ class MapMenuWidget extends StatelessWidget {
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: _kBg.withOpacity(0.88),
|
||||
color: _kBg.withValues(alpha: 0.88),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: _kCyan.withOpacity(0.25), width: 1),
|
||||
border: Border.all(color: _kCyan.withValues(alpha: 0.25), width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: _kCyan.withOpacity(0.12),
|
||||
color: _kCyan.withValues(alpha: 0.12),
|
||||
blurRadius: 16,
|
||||
),
|
||||
],
|
||||
@@ -106,7 +103,7 @@ class MapMenuWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
// ── القائمة الجانبية ─────────────────────────────────────────────────────
|
||||
Widget _buildSideMenu(MapPassengerController controller) {
|
||||
Widget _buildSideMenu(MapEngineController controller) {
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 420),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
@@ -120,13 +117,13 @@ class MapMenuWidget extends StatelessWidget {
|
||||
width: Get.width * 0.8,
|
||||
constraints: const BoxConstraints(maxWidth: 320),
|
||||
decoration: BoxDecoration(
|
||||
color: _kBg.withOpacity(0.97),
|
||||
color: _kBg.withValues(alpha: 0.97),
|
||||
border: Border(
|
||||
right: BorderSide(color: _kCyan.withOpacity(0.12), width: 1),
|
||||
right: BorderSide(color: _kCyan.withValues(alpha: 0.12), width: 1),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
color: Colors.black.withValues(alpha: 0.5),
|
||||
blurRadius: 32,
|
||||
),
|
||||
],
|
||||
@@ -239,7 +236,7 @@ class MapMenuWidget extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: _kBgSurface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: _kCyan.withOpacity(0.15), width: 1),
|
||||
border: Border.all(color: _kCyan.withValues(alpha: 0.15), width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -255,12 +252,12 @@ class MapMenuWidget extends StatelessWidget {
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
_kCyan.withOpacity(0.2),
|
||||
_kAmber.withOpacity(0.12),
|
||||
_kCyan.withValues(alpha: 0.2),
|
||||
_kAmber.withValues(alpha: 0.12),
|
||||
],
|
||||
),
|
||||
border:
|
||||
Border.all(color: _kCyan.withOpacity(0.35), width: 1.5),
|
||||
Border.all(color: _kCyan.withValues(alpha: 0.35), width: 1.5),
|
||||
),
|
||||
child: Icon(Icons.person_rounded, color: _kCyan, size: 28),
|
||||
),
|
||||
@@ -277,7 +274,7 @@ class MapMenuWidget extends StatelessWidget {
|
||||
border: Border.all(color: _kBg, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF00E676).withOpacity(0.5),
|
||||
color: const Color(0xFF00E676).withValues(alpha: 0.5),
|
||||
blurRadius: 6,
|
||||
),
|
||||
],
|
||||
@@ -365,7 +362,7 @@ class MapMenuWidget extends StatelessWidget {
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
_kCyan.withOpacity(0.15),
|
||||
_kCyan.withValues(alpha: 0.15),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
@@ -419,7 +416,7 @@ class _QuickBtn extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: _kBgSurface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: _kCyan.withOpacity(0.12), width: 1),
|
||||
border: Border.all(color: _kCyan.withValues(alpha: 0.12), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -463,7 +460,7 @@ class MenuListItem extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final iconColor = isDestructive
|
||||
? const Color(0xFFFF5252)
|
||||
: (color ?? _kCyan.withOpacity(0.80));
|
||||
: (color ?? _kCyan.withValues(alpha: 0.80));
|
||||
final textColor =
|
||||
isDestructive ? const Color(0xFFFF5252) : (color ?? _kText);
|
||||
|
||||
@@ -472,8 +469,8 @@ class MenuListItem extends StatelessWidget {
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
splashColor: _kCyan.withOpacity(0.07),
|
||||
highlightColor: _kCyan.withOpacity(0.04),
|
||||
splashColor: _kCyan.withValues(alpha: 0.07),
|
||||
highlightColor: _kCyan.withValues(alpha: 0.04),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
@@ -484,8 +481,8 @@ class MenuListItem extends StatelessWidget {
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: isDestructive
|
||||
? const Color(0xFFFF5252).withOpacity(0.08)
|
||||
: _kCyan.withOpacity(0.07),
|
||||
? const Color(0xFFFF5252).withValues(alpha: 0.08)
|
||||
: _kCyan.withValues(alpha: 0.07),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, size: 19, color: iconColor),
|
||||
@@ -504,7 +501,7 @@ class MenuListItem extends StatelessWidget {
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: _kTextMuted.withOpacity(0.4),
|
||||
color: _kTextMuted.withValues(alpha: 0.4),
|
||||
size: 18,
|
||||
),
|
||||
],
|
||||
@@ -520,7 +517,7 @@ class _MenuGridPainter extends CustomPainter {
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = AppColor.cyanBlue.withOpacity(0.04)
|
||||
..color = AppColor.cyanBlue.withValues(alpha: 0.04)
|
||||
..strokeWidth = 0.5;
|
||||
const spacing = 36.0;
|
||||
for (double y = 0; y < size.height; y += spacing) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
class MenuIconMapPageWidget extends StatelessWidget {
|
||||
@@ -13,7 +13,7 @@ class MenuIconMapPageWidget extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
return GetBuilder<MapEngineController>(
|
||||
builder: (controller) => Positioned(
|
||||
top: Get.height * .008,
|
||||
left: box.read(BoxName.lang) != 'ar' ? 5 : null,
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'dart:ui'; // مهم لإضافة تأثير الضبابية
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/location_search_controller.dart';
|
||||
|
||||
// --- الويدجت الرئيسية بالتصميم الجديد ---
|
||||
class PassengerRideLocationWidget extends StatefulWidget {
|
||||
@@ -43,7 +43,7 @@ class _PassengerRideLocationWidgetState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
return GetBuilder<LocationSearchController>(builder: (controller) {
|
||||
// --- نفس شرط الإظهار الخاص بك ---
|
||||
return AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
@@ -60,9 +60,9 @@ class _PassengerRideLocationWidgetState
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor.withOpacity(0.85),
|
||||
color: AppColor.secondaryColor.withValues(alpha: 0.85),
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
border: Border.all(color: AppColor.writeColor.withOpacity(0.2)),
|
||||
border: Border.all(color: AppColor.writeColor.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -89,7 +89,7 @@ class _PassengerRideLocationWidgetState
|
||||
Text(
|
||||
"Move the map to adjust the pin".tr,
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: AppColor.writeColor.withOpacity(0.7),
|
||||
color: AppColor.writeColor.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -8,8 +8,7 @@ import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/functions/digit_obsecur_formate.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
|
||||
class PaymentMethodPage extends StatelessWidget {
|
||||
const PaymentMethodPage({
|
||||
@@ -18,7 +17,7 @@ class PaymentMethodPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
return GetBuilder<MapEngineController>(
|
||||
builder: (controller) => Positioned(
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
|
||||
@@ -4,20 +4,23 @@ import 'package:Intaleq/constant/table_names.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/location_search_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
import 'form_search_places_destenation.dart';
|
||||
|
||||
class PickerAnimtionContainerFormPlaces extends StatelessWidget {
|
||||
PickerAnimtionContainerFormPlaces({
|
||||
super.key,
|
||||
});
|
||||
final controller = MapPassengerController();
|
||||
const PickerAnimtionContainerFormPlaces({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// DbSql sql = DbSql.instance;
|
||||
return GetBuilder<MapPassengerController>(
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
|
||||
return GetBuilder<MapEngineController>(
|
||||
builder: (controller) => Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
@@ -101,8 +104,7 @@ class PickerAnimtionContainerFormPlaces extends StatelessWidget {
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment
|
||||
.center,
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons
|
||||
@@ -132,9 +134,9 @@ class PickerAnimtionContainerFormPlaces extends StatelessWidget {
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await controller
|
||||
await rideLifecycle
|
||||
.getDirectionMap(
|
||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
||||
'${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}',
|
||||
'${favoritePlaces[index]['latitude']},${favoritePlaces[index]['longitude']}',
|
||||
);
|
||||
controller
|
||||
@@ -143,7 +145,7 @@ class PickerAnimtionContainerFormPlaces extends StatelessWidget {
|
||||
.changeBottomSheetShown(
|
||||
forceValue:
|
||||
true);
|
||||
controller
|
||||
rideLifecycle
|
||||
.bottomSheet();
|
||||
Get.back();
|
||||
},
|
||||
@@ -189,24 +191,22 @@ class PickerAnimtionContainerFormPlaces extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
if (controller.isPickerShown &&
|
||||
controller.placesDestination.isEmpty)
|
||||
locationSearch.placesDestination.isEmpty)
|
||||
MyElevatedButton(
|
||||
title: 'Go to this Target'.tr,
|
||||
onPressed: () async {
|
||||
await controller.getDirectionMap(
|
||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
||||
'${controller.newMyLocation.latitude},${controller.newMyLocation.longitude}',
|
||||
await rideLifecycle.getDirectionMap(
|
||||
'${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}',
|
||||
'${locationSearch.newMyLocation.latitude},${locationSearch.newMyLocation.longitude}',
|
||||
);
|
||||
controller.changePickerShown();
|
||||
controller.changeBottomSheetShown(
|
||||
forceValue: true);
|
||||
controller.bottomSheet();
|
||||
// await sql
|
||||
// .getAllData(TableName.placesFavorite)
|
||||
rideLifecycle.bottomSheet();
|
||||
},
|
||||
),
|
||||
if (controller.isPickerShown &&
|
||||
controller.placesDestination.isEmpty)
|
||||
locationSearch.placesDestination.isEmpty)
|
||||
const SizedBox(),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,21 +3,25 @@ import 'package:get/get.dart';
|
||||
import 'package:Intaleq/constant/style.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/location_search_controller.dart';
|
||||
import '../../../controller/home/map/map_engine_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../../controller/home/points_for_rider_controller.dart';
|
||||
|
||||
class PointsPageForRider extends StatelessWidget {
|
||||
PointsPageForRider({
|
||||
super.key,
|
||||
});
|
||||
MapPassengerController mapPassengerController =
|
||||
Get.put(MapPassengerController());
|
||||
|
||||
final locationSearch = Get.find<LocationSearchController>();
|
||||
final mapEngine = Get.find<MapEngineController>();
|
||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(WayPointController());
|
||||
Get.find<WayPointController>();
|
||||
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
return Positioned(
|
||||
bottom: 2,
|
||||
left: 2,
|
||||
@@ -34,7 +38,7 @@ class PointsPageForRider extends StatelessWidget {
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
mapPassengerController.downPoints();
|
||||
mapEngine.downPoints();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_drop_down_circle_outlined),
|
||||
),
|
||||
@@ -52,7 +56,7 @@ class PointsPageForRider extends StatelessWidget {
|
||||
wayPointController.wayPoints.length > 1
|
||||
? ElevatedButton(
|
||||
onPressed: () async {
|
||||
mapPassengerController
|
||||
locationSearch
|
||||
.getMapPointsForAllMethods();
|
||||
},
|
||||
child: const Text('Get Direction'),
|
||||
@@ -74,7 +78,6 @@ class PointsPageForRider extends StatelessWidget {
|
||||
.entries
|
||||
.map((entry) {
|
||||
final index = entry.key;
|
||||
final wayPoint = entry.value;
|
||||
return Padding(
|
||||
key: ValueKey(index),
|
||||
padding: const EdgeInsets.all(1),
|
||||
@@ -98,7 +101,7 @@ class PointsPageForRider extends StatelessWidget {
|
||||
content: SizedBox(
|
||||
width: Get.width,
|
||||
height: 400,
|
||||
child: mapPassengerController
|
||||
child: locationSearch
|
||||
.placeListResponse[index]),
|
||||
);
|
||||
},
|
||||
@@ -106,13 +109,13 @@ class PointsPageForRider extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(),
|
||||
color:
|
||||
AppColor.accentColor.withOpacity(.5)),
|
||||
AppColor.accentColor.withValues(alpha: 0.5)),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(index > 0
|
||||
? mapPassengerController
|
||||
? locationSearch
|
||||
.currentLocationStringAll[index]
|
||||
.toString()
|
||||
: ''),
|
||||
@@ -239,7 +242,6 @@ class PointsPageForRider extends StatelessWidget {
|
||||
}
|
||||
|
||||
void showAddLocationDialog(BuildContext context, int index) {
|
||||
final TextEditingController locationController = TextEditingController();
|
||||
// Get.put(WayPointController());
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -298,26 +300,24 @@ class AppBarPointsPageForRider extends StatelessWidget {
|
||||
color: AppColor.primaryColor,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
maxRadius: 15,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: AppColor.secondaryColor,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
maxRadius: 15,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
color: AppColor.secondaryColor,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
"Switch Rider".tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
child: Text(
|
||||
"Switch Rider".tr,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Icon(
|
||||
Icons.clear,
|
||||
|
||||
@@ -11,7 +11,9 @@ import '../../../constant/style.dart';
|
||||
import '../../../controller/functions/audio_record1.dart';
|
||||
import '../../../controller/functions/launch.dart';
|
||||
import '../../../controller/functions/toast.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../../controller/home/map/ui_interactions_controller.dart';
|
||||
import '../../../controller/home/map/ride_state.dart';
|
||||
import '../../../controller/profile/profile_controller.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../views/home/profile/complaint_page.dart';
|
||||
@@ -24,9 +26,10 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
final ProfileController profileController = Get.put(ProfileController());
|
||||
final AudioRecorderController audioController =
|
||||
Get.put(AudioRecorderController());
|
||||
final uiController = Get.find<UiInteractionsController>();
|
||||
|
||||
return Obx(() {
|
||||
final controller = Get.find<MapPassengerController>();
|
||||
final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
// شرط الإظهار
|
||||
final bool isVisible =
|
||||
@@ -50,8 +53,8 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Get.isDarkMode
|
||||
? Colors.black.withOpacity(0.4)
|
||||
: Colors.black.withOpacity(0.1),
|
||||
? Colors.black.withValues(alpha: 0.4)
|
||||
: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 2,
|
||||
offset: const Offset(0, -3),
|
||||
@@ -69,7 +72,7 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.grayColor.withOpacity(0.3),
|
||||
color: AppColor.grayColor.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
@@ -85,7 +88,7 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 0.5,
|
||||
color: AppColor.grayColor.withOpacity(0.2)),
|
||||
color: AppColor.grayColor.withValues(alpha: 0.2)),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -104,7 +107,7 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
}
|
||||
|
||||
// --- الهيدر (بدون تغيير، ممتاز) ---
|
||||
Widget _buildCompactHeader(MapPassengerController controller) {
|
||||
Widget _buildCompactHeader(RideLifecycleController controller) {
|
||||
return Row(
|
||||
children: [
|
||||
// صورة السائق
|
||||
@@ -112,7 +115,7 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColor.primaryColor.withOpacity(0.5), width: 1.5),
|
||||
color: AppColor.primaryColor.withValues(alpha: 0.5), width: 1.5),
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 24,
|
||||
@@ -166,9 +169,9 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.writeColor.withOpacity(0.05),
|
||||
color: AppColor.writeColor.withValues(alpha: 0.05),
|
||||
border: Border.all(
|
||||
color: AppColor.grayColor.withOpacity(0.2)),
|
||||
color: AppColor.grayColor.withValues(alpha: 0.2)),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
@@ -190,7 +193,7 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor.withOpacity(0.08),
|
||||
color: AppColor.primaryColor.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
@@ -216,9 +219,10 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
// --- الأزرار (بدون تغيير) ---
|
||||
Widget _buildCompactActionButtons(
|
||||
BuildContext context,
|
||||
MapPassengerController controller,
|
||||
RideLifecycleController controller,
|
||||
ProfileController profileController,
|
||||
AudioRecorderController audioController) {
|
||||
final uiController = Get.find<UiInteractionsController>();
|
||||
return SizedBox(
|
||||
height: 60,
|
||||
child: Row(
|
||||
@@ -228,7 +232,7 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
icon: Icons.sos_rounded,
|
||||
label: 'SOS'.tr,
|
||||
color: AppColor.redColor,
|
||||
bgColor: AppColor.redColor.withOpacity(0.1),
|
||||
bgColor: AppColor.redColor.withValues(alpha: 0.1),
|
||||
onTap: () async {
|
||||
if (box.read(BoxName.sosPhonePassenger) == null) {
|
||||
await profileController.updatField(
|
||||
@@ -244,24 +248,26 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
icon: FontAwesome.whatsapp,
|
||||
label: 'WhatsApp'.tr,
|
||||
color: const Color(0xFF25D366),
|
||||
bgColor: const Color(0xFF25D366).withOpacity(0.1),
|
||||
bgColor: const Color(0xFF25D366).withValues(alpha: 0.1),
|
||||
onTap: () async {
|
||||
if (box.read(BoxName.sosPhonePassenger) == null) {
|
||||
await profileController.updatField(
|
||||
'sosPhone', TextInputType.phone);
|
||||
final phone = box.read(BoxName.sosPhonePassenger);
|
||||
if (phone == null || phone.toString().isEmpty) {
|
||||
// لا يوجد رقم طوارئ — نعرض الديالوج لإدخاله
|
||||
await uiController.shareTripWithFamily();
|
||||
} else {
|
||||
final phone = controller.formatSyrianPhoneNumber(
|
||||
box.read(BoxName.sosPhonePassenger).toString());
|
||||
controller.sendWhatsapp(phone);
|
||||
final formattedPhone = uiController.formatSyrianPhoneNumber(
|
||||
phone.toString());
|
||||
uiController.sendWhatsapp(formattedPhone);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
_compactBtn(
|
||||
icon: Icons.share,
|
||||
label: 'Share'.tr,
|
||||
color: AppColor.primaryColor,
|
||||
bgColor: AppColor.primaryColor.withOpacity(0.1),
|
||||
onTap: () async => await controller.shareTripWithFamily(),
|
||||
bgColor: AppColor.primaryColor.withValues(alpha: 0.1),
|
||||
onTap: () async => await uiController.shareTripWithFamily(),
|
||||
),
|
||||
GetBuilder<AudioRecorderController>(
|
||||
init: audioController,
|
||||
@@ -275,15 +281,19 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
? AppColor.redColor
|
||||
: AppColor.primaryColor,
|
||||
bgColor: audioCtx.isRecording
|
||||
? AppColor.redColor.withOpacity(0.1)
|
||||
: AppColor.primaryColor.withOpacity(0.1),
|
||||
? AppColor.redColor.withValues(alpha: 0.1)
|
||||
: AppColor.primaryColor.withValues(alpha: 0.1),
|
||||
onTap: () async {
|
||||
if (!audioCtx.isRecording) {
|
||||
await audioCtx.startRecording();
|
||||
Toast.show(context, 'Start Record'.tr, AppColor.greenColor);
|
||||
await audioCtx.startRecording(rideId: controller.rideId);
|
||||
if (context.mounted) {
|
||||
Toast.show(context, 'Start Record'.tr, AppColor.greenColor);
|
||||
}
|
||||
} else {
|
||||
await audioCtx.stopRecording();
|
||||
Toast.show(context, 'Record saved'.tr, AppColor.greenColor);
|
||||
if (context.mounted) {
|
||||
Toast.show(context, 'Record saved'.tr, AppColor.greenColor);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -293,7 +303,7 @@ class RideBeginPassenger extends StatelessWidget {
|
||||
icon: Icons.info_outline_rounded,
|
||||
label: 'Report'.tr,
|
||||
color: AppColor.grayColor,
|
||||
bgColor: AppColor.writeColor.withOpacity(0.1),
|
||||
bgColor: AppColor.writeColor.withValues(alpha: 0.1),
|
||||
onTap: () => Get.to(() => ComplaintPage()),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -8,7 +8,9 @@ import '../../../constant/box_name.dart';
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/links.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../../controller/home/map/ui_interactions_controller.dart';
|
||||
import '../../../controller/home/map/ride_state.dart';
|
||||
import '../../../controller/profile/profile_controller.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
@@ -18,8 +20,10 @@ class RideFromStartApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final profileController = Get.put(ProfileController());
|
||||
final MapPassengerController controller =
|
||||
Get.find<MapPassengerController>();
|
||||
final RideLifecycleController controller =
|
||||
Get.find<RideLifecycleController>();
|
||||
final UiInteractionsController uiController =
|
||||
Get.find<UiInteractionsController>();
|
||||
|
||||
return Obx(() {
|
||||
final bool isRideActive =
|
||||
@@ -59,7 +63,7 @@ class RideFromStartApp extends StatelessWidget {
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Get.isDarkMode
|
||||
? Colors.black.withOpacity(0.4)
|
||||
? Colors.black.withValues(alpha: 0.4)
|
||||
: Colors.black12,
|
||||
blurRadius: 15.0,
|
||||
spreadRadius: 5.0,
|
||||
@@ -78,7 +82,7 @@ class RideFromStartApp extends StatelessWidget {
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.grayColor.withOpacity(0.3),
|
||||
color: AppColor.grayColor.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
@@ -134,7 +138,7 @@ class RideFromStartApp extends StatelessWidget {
|
||||
Container(
|
||||
width: 1,
|
||||
height: 12,
|
||||
color: AppColor.grayColor.withOpacity(0.3)),
|
||||
color: AppColor.grayColor.withValues(alpha: 0.3)),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"$carType - $carModel",
|
||||
@@ -160,7 +164,7 @@ class RideFromStartApp extends StatelessWidget {
|
||||
const EdgeInsets.symmetric(vertical: 12, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.grayColor
|
||||
.withOpacity(0.1), // خلفية رمادية خفيفة جداً
|
||||
.withValues(alpha: 0.1), // خلفية رمادية خفيفة جداً
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Row(
|
||||
@@ -188,7 +192,7 @@ class RideFromStartApp extends StatelessWidget {
|
||||
flex: 2,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _checkAndCall(
|
||||
controller.sendWhatsapp, profileController),
|
||||
uiController.sendWhatsapp, profileController),
|
||||
icon:
|
||||
const Icon(FontAwesome.whatsapp, color: Colors.white),
|
||||
label: Text("Share Trip".tr,
|
||||
@@ -252,9 +256,9 @@ class RideFromStartApp extends StatelessWidget {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withOpacity(0.5)),
|
||||
border: Border.all(color: color.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
@@ -297,7 +301,7 @@ class RideFromStartApp extends StatelessWidget {
|
||||
return Container(
|
||||
height: 30,
|
||||
width: 1,
|
||||
color: AppColor.grayColor.withOpacity(0.2),
|
||||
color: AppColor.grayColor.withValues(alpha: 0.2),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:Intaleq/constant/colors.dart';
|
||||
import 'package:Intaleq/constant/style.dart';
|
||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_state.dart';
|
||||
|
||||
// --- الويدجت الرئيسية بالتصميم الجديد ---
|
||||
class SearchingCaptainWindow extends StatefulWidget {
|
||||
@@ -36,7 +37,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
||||
// [تعديل 1] نستخدم Obx للاستماع إلى التغييرات في حالة الرحلة
|
||||
return Obx(() {
|
||||
// ابحث عن الكنترولر مرة واحدة
|
||||
final controller = Get.find<MapPassengerController>();
|
||||
final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
// [تعديل 2] شرط الإظهار يعتمد الآن على حالة الرحلة مباشرة
|
||||
final bool isVisible =
|
||||
@@ -58,7 +59,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, -5),
|
||||
),
|
||||
@@ -83,7 +84,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColor.writeColor,
|
||||
side:
|
||||
BorderSide(color: AppColor.writeColor.withOpacity(0.3)),
|
||||
BorderSide(color: AppColor.writeColor.withValues(alpha: 0.3)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
@@ -99,7 +100,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
||||
}
|
||||
|
||||
// --- ويدجت بناء أنيميشن الرادار ---
|
||||
Widget _buildRadarAnimation(MapPassengerController controller) {
|
||||
Widget _buildRadarAnimation(RideLifecycleController controller) {
|
||||
return SizedBox(
|
||||
height: 180, // ارتفاع ثابت لمنطقة الأنيميشن
|
||||
child: Stack(
|
||||
@@ -125,7 +126,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColor.primaryColor.withOpacity(0.7),
|
||||
color: AppColor.primaryColor.withValues(alpha: 0.7),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
@@ -147,7 +148,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
||||
Text(
|
||||
'Searching for the nearest captain...'.tr,
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(color: AppColor.writeColor.withOpacity(0.7)),
|
||||
.copyWith(color: AppColor.writeColor.withValues(alpha: 0.7)),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -166,12 +167,12 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
color: AppColor.primaryColor,
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.2),
|
||||
backgroundColor: AppColor.primaryColor.withValues(alpha: 0.2),
|
||||
),
|
||||
Center(
|
||||
child: Icon(
|
||||
Icons.search,
|
||||
color: AppColor.writeColor.withOpacity(0.8),
|
||||
color: AppColor.writeColor.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:Intaleq/constant/colors.dart';
|
||||
import 'package:Intaleq/constant/style.dart';
|
||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
||||
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -10,8 +10,10 @@ import '../../../constant/links.dart';
|
||||
import '../../../print.dart';
|
||||
|
||||
class CupertinoDriverListWidget extends StatelessWidget {
|
||||
MapPassengerController mapPassengerController =
|
||||
Get.put(MapPassengerController());
|
||||
CupertinoDriverListWidget({super.key});
|
||||
|
||||
final RideLifecycleController mapPassengerController =
|
||||
Get.find<RideLifecycleController>();
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
@@ -158,7 +160,7 @@ class CupertinoDriverListWidget extends StatelessWidget {
|
||||
),
|
||||
onTap: () {
|
||||
Log.print(' driver["id"]: ${driver['driver_id']}');
|
||||
Get.find<MapPassengerController>().driverIdVip =
|
||||
Get.find<RideLifecycleController>().driverIdVip =
|
||||
driver['driver_id'];
|
||||
|
||||
// Handle driver selection
|
||||
@@ -266,8 +268,6 @@ class CupertinoDriverListWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
void showDateTimePickerDialog(Map<String, dynamic> driver) {
|
||||
DateTime selectedDateTime = DateTime.now();
|
||||
|
||||
Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: "Select date and time of trip".tr,
|
||||
@@ -302,7 +302,9 @@ class CupertinoDriverListWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
class DateTimePickerWidget extends StatelessWidget {
|
||||
final MapPassengerController controller = Get.put(MapPassengerController());
|
||||
DateTimePickerWidget({super.key});
|
||||
|
||||
final RideLifecycleController controller = Get.find<RideLifecycleController>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
|
||||
GetBuilder<MapPassengerController> timerForCancelTripFromPassenger() {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
GetBuilder<RideLifecycleController> timerForCancelTripFromPassenger() {
|
||||
return GetBuilder<RideLifecycleController>(
|
||||
builder: (controller) {
|
||||
final isNearEnd =
|
||||
controller.remainingTime <= 5; // Define a threshold for "near end"
|
||||
|
||||
@@ -4,7 +4,7 @@ import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../constant/style.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import 'ride_begin_passenger.dart';
|
||||
|
||||
class TimerToPassengerFromDriver extends StatelessWidget {
|
||||
@@ -14,7 +14,7 @@ class TimerToPassengerFromDriver extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
if (controller.remainingTime == 0 &&
|
||||
(controller.isDriverInPassengerWay == true ||
|
||||
controller.timeToPassengerFromDriverAfterApplied > 0)) {
|
||||
|
||||
@@ -12,7 +12,8 @@ import '../../../constant/style.dart';
|
||||
import '../../../controller/functions/audio_record1.dart';
|
||||
import '../../../controller/functions/launch.dart';
|
||||
import '../../../controller/functions/toast.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||
import '../../../controller/home/map/ui_interactions_controller.dart';
|
||||
|
||||
class VipRideBeginPassenger extends StatelessWidget {
|
||||
const VipRideBeginPassenger({
|
||||
@@ -24,8 +25,8 @@ class VipRideBeginPassenger extends StatelessWidget {
|
||||
ProfileController profileController = Get.put(ProfileController());
|
||||
AudioRecorderController audioController =
|
||||
Get.put(AudioRecorderController());
|
||||
// Get.put(MapPassengerController());
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
final uiController = Get.find<UiInteractionsController>();
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
if (controller.statusRideVip == 'Begin' ||
|
||||
!controller.statusRideFromStart) {
|
||||
return Positioned(
|
||||
@@ -148,9 +149,11 @@ class VipRideBeginPassenger extends StatelessWidget {
|
||||
child: audioController.isRecording == false
|
||||
? IconButton(
|
||||
onPressed: () async {
|
||||
await audioController.startRecording();
|
||||
Toast.show(context, 'Start Record'.tr,
|
||||
AppColor.greenColor);
|
||||
await audioController.startRecording(rideId: controller.rideId);
|
||||
if (context.mounted) {
|
||||
Toast.show(context, 'Start Record'.tr,
|
||||
AppColor.greenColor);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.play_circle_fill_outlined,
|
||||
@@ -162,8 +165,10 @@ class VipRideBeginPassenger extends StatelessWidget {
|
||||
: IconButton(
|
||||
onPressed: () async {
|
||||
await audioController.stopRecording();
|
||||
Toast.show(context, 'Record saved'.tr,
|
||||
AppColor.greenColor);
|
||||
if (context.mounted) {
|
||||
Toast.show(context, 'Record saved'.tr,
|
||||
AppColor.greenColor);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.stop_circle,
|
||||
@@ -215,15 +220,11 @@ class VipRideBeginPassenger extends StatelessWidget {
|
||||
profileController.prfoileData['sosPhone']);
|
||||
}
|
||||
} else {
|
||||
String phoneNumber = box
|
||||
.read(BoxName.sosPhonePassenger)
|
||||
.toString();
|
||||
// phoneNumber = phoneNumber.replaceAll('0', '');
|
||||
var phone = box.read(BoxName.countryCode) ==
|
||||
'Egypt'
|
||||
? '+2${box.read(BoxName.sosPhonePassenger)}'
|
||||
: '+962${box.read(BoxName.sosPhonePassenger)}';
|
||||
controller.sendWhatsapp(phone);
|
||||
uiController.sendWhatsapp(phone);
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
@@ -237,7 +238,7 @@ class VipRideBeginPassenger extends StatelessWidget {
|
||||
width: Get.width * .15,
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
await controller.shareTripWithFamily();
|
||||
await uiController.shareTripWithFamily();
|
||||
},
|
||||
icon: const Icon(
|
||||
AntDesign.Safety,
|
||||
@@ -283,13 +284,12 @@ class VipRideBeginPassenger extends StatelessWidget {
|
||||
}
|
||||
|
||||
class StreamCounter extends StatelessWidget {
|
||||
const StreamCounter({Key? key}) : super(key: key);
|
||||
const StreamCounter({super.key});
|
||||
|
||||
@override
|
||||
// Build the UI based on the timer value
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(MapPassengerController());
|
||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
return StreamBuilder<int>(
|
||||
initialData: 0,
|
||||
stream: controller.timerController.stream,
|
||||
|
||||
@@ -268,9 +268,6 @@ class NavigationController extends GetxController
|
||||
},
|
||||
];
|
||||
|
||||
static final String _routeApiBaseUrl =
|
||||
"${AppLink.routesOsm}/route/v1/driving";
|
||||
|
||||
IconData get currentManeuverIcon {
|
||||
switch (currentManeuverModifier) {
|
||||
case 4: // Arrive
|
||||
@@ -378,7 +375,6 @@ class NavigationController extends GetxController
|
||||
void onMapCreated(IntaleqMapController controller) async {
|
||||
Log.print("DEBUG: NavigationController.onMapCreated called");
|
||||
mapController = controller;
|
||||
await onStyleLoaded();
|
||||
}
|
||||
|
||||
Future<void> onStyleLoaded() async {
|
||||
@@ -577,7 +573,7 @@ class NavigationController extends GetxController
|
||||
}
|
||||
|
||||
void _checkOffRoute(LatLng pos) {
|
||||
if (_autoRecalcInProgress || isLoading) return;
|
||||
if (!isNavigating || _autoRecalcInProgress || isLoading) return;
|
||||
if (_fullRouteCoordinates.isEmpty) return;
|
||||
|
||||
const int searchWindow = 80;
|
||||
@@ -604,7 +600,6 @@ class NavigationController extends GetxController
|
||||
if (elapsed >= _offRouteTriggerSeconds) {
|
||||
_offRouteStartTime = null;
|
||||
_autoRecalcInProgress = true;
|
||||
// Smart reroute: check if we have alternative routes available
|
||||
_smartRecalculateRoute(pos);
|
||||
}
|
||||
}
|
||||
@@ -613,40 +608,11 @@ class NavigationController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
/// الحل الذكي: إذا كان هناك مسارات بديلة متاحة، اختر الأقرب.
|
||||
/// وإلا فاطلب مسار جديد من الموقع الحالي إلى الوجهة.
|
||||
/// Recalculate immediately from the latest GPS point to the destination.
|
||||
Future<void> _smartRecalculateRoute(LatLng currentPos) async {
|
||||
try {
|
||||
// Check if we have alternative routes
|
||||
if (routes.isNotEmpty && selectedRouteIndex < routes.length - 1) {
|
||||
// Try using the next alternative route
|
||||
final nextIndex = selectedRouteIndex + 1;
|
||||
final nextRoute = routes[nextIndex];
|
||||
|
||||
// Calculate distance from current position to this alternative route's start
|
||||
double minDist = double.infinity;
|
||||
for (var coord in nextRoute.coordinates) {
|
||||
final d = Geolocator.distanceBetween(
|
||||
currentPos.latitude,
|
||||
currentPos.longitude,
|
||||
coord.latitude,
|
||||
coord.longitude,
|
||||
);
|
||||
if (d < minDist) minDist = d;
|
||||
}
|
||||
|
||||
// If this alternative is reasonable, switch to it
|
||||
if (minDist < 100) {
|
||||
selectRoute(nextIndex);
|
||||
Log.print("DEBUG: Switched to alternative route due to deviation");
|
||||
_autoRecalcInProgress = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No good alternative, recalculate from current position to destination
|
||||
if (_finalDestination != null) {
|
||||
await recalculateRoute();
|
||||
await recalculateRoute(origin: currentPos, keepNavigationActive: true);
|
||||
}
|
||||
_autoRecalcInProgress = false;
|
||||
} catch (e) {
|
||||
@@ -906,7 +872,8 @@ class NavigationController extends GetxController
|
||||
return const LatLng(31.7225, 35.9933); // Queen Alia Airport (JO)
|
||||
}
|
||||
|
||||
Future<void> getRoute(LatLng origin, LatLng destination) async {
|
||||
Future<void> getRoute(LatLng origin, LatLng destination,
|
||||
{bool keepNavigationActive = false}) async {
|
||||
isLoading = true;
|
||||
update();
|
||||
|
||||
@@ -1007,9 +974,8 @@ class NavigationController extends GetxController
|
||||
currentStepIndex = 0;
|
||||
_nextInstructionSpoken = false;
|
||||
|
||||
// Don't start navigating immediately, wait for user to press Start
|
||||
isNavigating = false;
|
||||
_cameraLockedToUser = false;
|
||||
isNavigating = keepNavigationActive;
|
||||
_cameraLockedToUser = keepNavigationActive;
|
||||
_offRouteStartTime = null;
|
||||
isLoading = false;
|
||||
|
||||
@@ -1032,7 +998,10 @@ class NavigationController extends GetxController
|
||||
// Re-add car marker after polyline updates (ensures it stays on top)
|
||||
if (isStyleLoaded) _updateCarMarker();
|
||||
|
||||
if (_fullRouteCoordinates.length >= 2) {
|
||||
if (keepNavigationActive && myLocation != null) {
|
||||
animateCameraToPosition(myLocation!,
|
||||
bearing: _smoothedHeading, zoom: _targetZoom, tilt: _targetTilt);
|
||||
} else if (_fullRouteCoordinates.length >= 2) {
|
||||
final bounds =
|
||||
data['bbox'] != null && (data['bbox'] as List).length == 4
|
||||
? LatLngBounds(
|
||||
@@ -1117,12 +1086,23 @@ class NavigationController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> recalculateRoute() async {
|
||||
if (myLocation == null || _finalDestination == null || isLoading) return;
|
||||
Future<void> recalculateRoute(
|
||||
{LatLng? origin, bool keepNavigationActive = false}) async {
|
||||
final LatLng? routeOrigin = origin ?? myLocation;
|
||||
if (routeOrigin == null || _finalDestination == null || isLoading) return;
|
||||
|
||||
isLoading = true;
|
||||
update();
|
||||
mySnackbarInfo('جاري حساب مسار جديد...');
|
||||
await getRoute(myLocation!, _finalDestination!);
|
||||
|
||||
markers = markers.where((m) => m.markerId.value != 'origin').toSet();
|
||||
markers.add(Marker(
|
||||
markerId: const MarkerId('origin'),
|
||||
position: routeOrigin,
|
||||
icon: InlqBitmap.fromStyleImage('start_icon'),
|
||||
));
|
||||
|
||||
await getRoute(routeOrigin, _finalDestination!,
|
||||
keepNavigationActive: keepNavigationActive);
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
@@ -1290,17 +1270,19 @@ class NavigationController extends GetxController
|
||||
try {
|
||||
// ✅ Use searchPlaces from intaleq_maps SDK
|
||||
final results = await mapController!.searchPlaces(q);
|
||||
|
||||
|
||||
if (myLocation != null) {
|
||||
for (final p in results) {
|
||||
final plat = double.tryParse(p['latitude']?.toString() ?? '0') ?? 0.0;
|
||||
final plng = double.tryParse(p['longitude']?.toString() ?? '0') ?? 0.0;
|
||||
p['distanceKm'] = _haversineKm(myLocation!.latitude, myLocation!.longitude, plat, plng);
|
||||
final plng =
|
||||
double.tryParse(p['longitude']?.toString() ?? '0') ?? 0.0;
|
||||
p['distanceKm'] = _haversineKm(
|
||||
myLocation!.latitude, myLocation!.longitude, plat, plng);
|
||||
}
|
||||
results.sort((a, b) =>
|
||||
(a['distanceKm'] as double).compareTo(b['distanceKm'] as double));
|
||||
}
|
||||
|
||||
|
||||
placesDestination = results;
|
||||
update();
|
||||
} catch (e) {
|
||||
|
||||
@@ -41,6 +41,7 @@ class NavigationView extends StatelessWidget {
|
||||
IntaleqMap(
|
||||
apiKey: Env.mapSaasKey,
|
||||
onMapCreated: c.onMapCreated,
|
||||
onStyleLoaded: c.onStyleLoaded,
|
||||
onLongPress: (pos) => c.onMapLongPressed(Point(0, 0), pos),
|
||||
onTap: (pos) => c.onMapTapped(Point(0, 0), pos),
|
||||
markers: c.markers,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
290
lib/views/widgets/voice_call_bottom_sheet.dart
Normal file
290
lib/views/widgets/voice_call_bottom_sheet.dart
Normal file
@@ -0,0 +1,290 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../controller/voice_call_controller.dart';
|
||||
|
||||
class VoiceCallBottomSheet extends StatelessWidget {
|
||||
const VoiceCallBottomSheet({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = Get.find<VoiceCallController>();
|
||||
final double screenHeight = MediaQuery.of(context).size.height;
|
||||
final bool isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
// Harmonious curated colors
|
||||
final Color bgColor = isDark ? const Color(0xFF121212) : Colors.white;
|
||||
final Color cardColor = isDark ? const Color(0xFF1E1E1E) : const Color(0xFFF5F5F7);
|
||||
final Color textColor = isDark ? Colors.white : const Color(0xFF1C1C1E);
|
||||
final Color subTextColor = isDark ? Colors.white70 : Colors.black54;
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () async => false,
|
||||
child: Container(
|
||||
height: screenHeight * 0.9,
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(32)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, -5),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Obx(() {
|
||||
final state = controller.state.value;
|
||||
final seconds = controller.elapsedSeconds.value;
|
||||
final remoteName = controller.remoteName.value;
|
||||
final isMuted = controller.isMuted.value;
|
||||
final isSpeakerOn = controller.isSpeakerOn.value;
|
||||
|
||||
// Progress ring logic
|
||||
final double progress = seconds / 60.0;
|
||||
final Color ringColor = seconds > 10 ? const Color(0xFF2ECC71) : const Color(0xFFE74C3C);
|
||||
|
||||
// Status text translations
|
||||
String statusText = "";
|
||||
switch (state) {
|
||||
case VoiceCallState.dialing:
|
||||
statusText = "${'Calling'.tr} $remoteName...";
|
||||
break;
|
||||
case VoiceCallState.ringing:
|
||||
statusText = "${'Captain'.tr} $remoteName ${'is calling you'.tr}...";
|
||||
break;
|
||||
case VoiceCallState.connecting:
|
||||
statusText = "Connecting...".tr;
|
||||
break;
|
||||
case VoiceCallState.active:
|
||||
statusText = "Call Connected".tr;
|
||||
break;
|
||||
case VoiceCallState.ended:
|
||||
statusText = "Call Ended".tr;
|
||||
break;
|
||||
case VoiceCallState.idle:
|
||||
statusText = "";
|
||||
break;
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Top Drag Handle Indicator
|
||||
Center(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 12, bottom: 24),
|
||||
width: 44,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.white24 : Colors.black12,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// Header Info
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
"Free Call".tr,
|
||||
style: TextStyle(
|
||||
color: ringColor,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 14,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
remoteName,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 26,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
statusText,
|
||||
style: TextStyle(
|
||||
color: subTextColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Avatar & Animated Progress Ring
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Progress ring around avatar (Active state only)
|
||||
if (state == VoiceCallState.active)
|
||||
SizedBox(
|
||||
width: 172,
|
||||
height: 172,
|
||||
child: CircularProgressIndicator(
|
||||
value: progress,
|
||||
strokeWidth: 5,
|
||||
backgroundColor: isDark ? Colors.white10 : Colors.black12,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(ringColor),
|
||||
),
|
||||
),
|
||||
|
||||
// Main Avatar Card
|
||||
Container(
|
||||
width: 150,
|
||||
height: 150,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: cardColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: remoteName.isNotEmpty
|
||||
? Text(
|
||||
remoteName[0].toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 54,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.person,
|
||||
color: textColor.withOpacity(0.6),
|
||||
size: 64,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Timer Counter Display
|
||||
if (state == VoiceCallState.active)
|
||||
Text(
|
||||
"0:${seconds.toString().padLeft(2, '0')}",
|
||||
style: TextStyle(
|
||||
color: seconds > 10 ? textColor : const Color(0xFFE74C3C),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 22,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Action Controls Block
|
||||
if (state == VoiceCallState.ringing)
|
||||
// Incoming Ringing Controls: Accept / Decline
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildCircleActionButton(
|
||||
icon: Icons.call_end_rounded,
|
||||
color: Colors.white,
|
||||
bgColor: const Color(0xFFE74C3C),
|
||||
onTap: () => controller.declineCall(),
|
||||
label: "Decline".tr,
|
||||
),
|
||||
_buildCircleActionButton(
|
||||
icon: Icons.call_rounded,
|
||||
color: Colors.white,
|
||||
bgColor: const Color(0xFF2ECC71),
|
||||
onTap: () => controller.acceptCall(),
|
||||
label: "Accept".tr,
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
// Dialing or Connected Controls: Speaker / Mute / Hangup
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// Speakerphone toggle
|
||||
_buildCircleActionButton(
|
||||
icon: isSpeakerOn ? Icons.volume_up_rounded : Icons.volume_down_rounded,
|
||||
color: isSpeakerOn ? Colors.white : textColor,
|
||||
bgColor: isSpeakerOn ? const Color(0xFF2ECC71) : cardColor,
|
||||
onTap: () => controller.toggleSpeaker(),
|
||||
label: "Speaker".tr,
|
||||
),
|
||||
// Hangup Call
|
||||
_buildCircleActionButton(
|
||||
icon: Icons.call_end_rounded,
|
||||
color: Colors.white,
|
||||
bgColor: const Color(0xFFE74C3C),
|
||||
onTap: () => controller.hangup(),
|
||||
label: "End".tr,
|
||||
),
|
||||
// Mute Microphone
|
||||
_buildCircleActionButton(
|
||||
icon: isMuted ? Icons.mic_off_rounded : Icons.mic_rounded,
|
||||
color: isMuted ? Colors.white : textColor,
|
||||
bgColor: isMuted ? const Color(0xFFE74C3C) : cardColor,
|
||||
onTap: () => controller.toggleMute(),
|
||||
label: "Mute".tr,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCircleActionButton({
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required Color bgColor,
|
||||
required VoidCallback onTap,
|
||||
required String label,
|
||||
}) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
padding: const EdgeInsets.all(18),
|
||||
backgroundColor: bgColor,
|
||||
foregroundColor: color,
|
||||
elevation: 2,
|
||||
),
|
||||
child: Icon(icon, size: 28),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user