Fixes & Updates - 2026-06-01: Integrate Back-End v3 updates, fix call/connection issues across apps
This commit is contained in:
File diff suppressed because one or more lines are too long
83
compare.sh
Normal file
83
compare.sh
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
ORIG_FILE="lib/controller/home/map_passenger_controller.dart"
|
||||||
|
SPLIT_DIR="lib/controller/home/map"
|
||||||
|
|
||||||
|
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
|
||||||
|
for method in $METHODS; do
|
||||||
|
# Search for this method name as a whole word in split controllers
|
||||||
|
FOUND=$(grep -rw "$method" "$SPLIT_DIR" 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 -rw "$var" "$SPLIT_DIR" 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"
|
||||||
79
compare_controllers.py
Normal file
79
compare_controllers.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
original_path = 'lib/controller/home/map_passenger_controller.dart'
|
||||||
|
split_dir = 'lib/controller/home/map'
|
||||||
|
|
||||||
|
# Read original file
|
||||||
|
with open(original_path, 'r', encoding='utf-8') as f:
|
||||||
|
orig_content = f.read()
|
||||||
|
|
||||||
|
# Read all split files
|
||||||
|
split_contents = {}
|
||||||
|
for filename in os.listdir(split_dir):
|
||||||
|
if filename.endswith('.dart') and filename != 'map_screen_binding.dart':
|
||||||
|
filepath = os.path.join(split_dir, filename)
|
||||||
|
with open(filepath, 'r', encoding='utf-8') as f:
|
||||||
|
split_contents[filename] = f.read()
|
||||||
|
|
||||||
|
# Combined content of all split files
|
||||||
|
combined_split_content = '\n'.join(split_contents.values())
|
||||||
|
|
||||||
|
# Regex to find method/function declarations inside a class in Dart
|
||||||
|
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 strip_comments(text):
|
||||||
|
text = re.sub(r'/\*.*?\*/', '', text, flags=re.DOTALL)
|
||||||
|
text = re.sub(r'//.*', '', text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
orig_clean = strip_comments(orig_content)
|
||||||
|
combined_split_clean = strip_comments(combined_split_content)
|
||||||
|
|
||||||
|
method_decl_pattern = re.compile(
|
||||||
|
r'(?:[a-zA-Z0-9_<>\?\[\]]+(?:\s+[a-zA-Z0-9_<>\?\[\]]+)*\s+)?([a-zA-Z0-9_]+)\s*\([^\)]*\)\s*(?:async)?\s*(?:=>|\{)'
|
||||||
|
)
|
||||||
|
|
||||||
|
original_methods = set()
|
||||||
|
for match in method_decl_pattern.finditer(orig_clean):
|
||||||
|
method_name = match.group(1)
|
||||||
|
if method_name not in keywords and not method_name.isdigit():
|
||||||
|
original_methods.add(method_name)
|
||||||
|
|
||||||
|
var_decl_pattern = re.compile(
|
||||||
|
r'\b(?:var|final|const|late|Rx[a-zA-Z]+|String|int|double|bool|List|Map|Set|Timer|LatLng|Position|IntaleqMapController)\??\s+([a-zA-Z0-9_]+)\b'
|
||||||
|
)
|
||||||
|
|
||||||
|
original_vars = set()
|
||||||
|
for match in var_decl_pattern.finditer(orig_clean):
|
||||||
|
var_name = match.group(1)
|
||||||
|
if var_name not in keywords and not var_name.isdigit():
|
||||||
|
original_vars.add(var_name)
|
||||||
|
|
||||||
|
missing_methods = []
|
||||||
|
for method in sorted(original_methods):
|
||||||
|
if not re.search(r'\b' + re.escape(method) + r'\b', combined_split_clean):
|
||||||
|
missing_methods.append(method)
|
||||||
|
|
||||||
|
missing_vars = []
|
||||||
|
for var in sorted(original_vars):
|
||||||
|
if not re.search(r'\b' + re.escape(var) + r'\b', combined_split_clean):
|
||||||
|
missing_vars.append(var)
|
||||||
|
|
||||||
|
print("--- MISSING METHODS ---")
|
||||||
|
print(f"Total original methods found: {len(original_methods)}")
|
||||||
|
print(f"Total missing: {len(missing_methods)}")
|
||||||
|
for m in missing_methods:
|
||||||
|
print(f" - {m}")
|
||||||
|
|
||||||
|
print("\n--- MISSING VARIABLES/FIELDS ---")
|
||||||
|
print(f"Total original variables found: {len(original_vars)}")
|
||||||
|
print(f"Total missing: {len(missing_vars)}")
|
||||||
|
for v in missing_vars:
|
||||||
|
print(f" - {v}")
|
||||||
0
comparison_results.txt
Normal file
0
comparison_results.txt
Normal file
@@ -46,6 +46,11 @@ post_install do |installer|
|
|||||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
|
||||||
'$(inherited)',
|
'$(inherited)',
|
||||||
'PERMISSION_CONTACTS=1',
|
'PERMISSION_CONTACTS=1',
|
||||||
|
'PERMISSION_LOCATION=1',
|
||||||
|
'PERMISSION_MICROPHONE=1',
|
||||||
|
'PERMISSION_NOTIFICATIONS=1',
|
||||||
|
'PERMISSION_CAMERA=1',
|
||||||
|
'PERMISSION_PHOTOS=1',
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- flutter_tts (0.0.1):
|
- flutter_tts (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- flutter_webrtc (1.4.0):
|
||||||
|
- Flutter
|
||||||
|
- WebRTC-SDK (= 144.7559.01)
|
||||||
- geolocator_apple (1.2.0):
|
- geolocator_apple (1.2.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -246,6 +249,7 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- wakelock_plus (0.0.1):
|
- wakelock_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- WebRTC-SDK (144.7559.01)
|
||||||
- webview_flutter_wkwebview (0.0.1):
|
- webview_flutter_wkwebview (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -264,6 +268,7 @@ DEPENDENCIES:
|
|||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
|
||||||
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
|
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
|
||||||
|
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
|
||||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
||||||
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
|
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
|
||||||
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
|
- image_cropper (from `.symlinks/plugins/image_cropper/ios`)
|
||||||
@@ -322,6 +327,7 @@ SPEC REPOS:
|
|||||||
- StripePaymentsUI
|
- StripePaymentsUI
|
||||||
- StripeUICore
|
- StripeUICore
|
||||||
- TOCropViewController
|
- TOCropViewController
|
||||||
|
- WebRTC-SDK
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
app_links:
|
app_links:
|
||||||
@@ -350,6 +356,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
|
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
|
||||||
flutter_tts:
|
flutter_tts:
|
||||||
:path: ".symlinks/plugins/flutter_tts/ios"
|
:path: ".symlinks/plugins/flutter_tts/ios"
|
||||||
|
flutter_webrtc:
|
||||||
|
:path: ".symlinks/plugins/flutter_webrtc/ios"
|
||||||
geolocator_apple:
|
geolocator_apple:
|
||||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
||||||
google_sign_in_ios:
|
google_sign_in_ios:
|
||||||
@@ -424,6 +432,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||||
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
|
||||||
flutter_tts: 35ac3c7d42412733e795ea96ad2d7e05d0a75113
|
flutter_tts: 35ac3c7d42412733e795ea96ad2d7e05d0a75113
|
||||||
|
flutter_webrtc: ec91d94b484ad49cf191ef93413f64a40ffd3b4c
|
||||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||||
google_sign_in_ios: 000870aa06da9b28d1d0bf7ef70ff0213059dd28
|
google_sign_in_ios: 000870aa06da9b28d1d0bf7ef70ff0213059dd28
|
||||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
@@ -467,8 +476,9 @@ SPEC CHECKSUMS:
|
|||||||
vibration: ca8104a8875b9c493e15b21b04e456befd0ff6eb
|
vibration: ca8104a8875b9c493e15b21b04e456befd0ff6eb
|
||||||
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
|
video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
|
WebRTC-SDK: ab9b5319e458c2bfebdc92b3600740da35d5630d
|
||||||
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
|
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
|
||||||
|
|
||||||
PODFILE CHECKSUM: 8a0b04ec79a0d49122ae6c10242e7cb023122802
|
PODFILE CHECKSUM: 2ba2e4898a3d9a1615dafa81db0705bd67da92c3
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -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/home/deep_link_controller.dart';
|
||||||
import 'package:Intaleq/controller/local/local_controller.dart';
|
import 'package:Intaleq/controller/local/local_controller.dart';
|
||||||
import 'package:Intaleq/controller/functions/tts.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.
|
/// This is the central dependency injection file for the app.
|
||||||
/// It uses GetX Bindings to make the app start faster and manage memory better.
|
/// 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
|
// TextToSpeechController for global accessibility
|
||||||
Get.lazyPut(() => TextToSpeechController(), fenix: true);
|
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 addMishwari = "$server/ride/mishwari/add.php";
|
||||||
static String cancelMishwari = "$server/ride/mishwari/cancel.php";
|
static String cancelMishwari = "$server/ride/mishwari/cancel.php";
|
||||||
static String getMishwari = "$server/ride/mishwari/get.php";
|
static String getMishwari = "$server/ride/mishwari/get.php";
|
||||||
|
static String sendChatMessage = "$server/ride/chat/send_message.php";
|
||||||
|
|
||||||
//-----------------DriverOrder------------------
|
//-----------------DriverOrder------------------
|
||||||
|
|
||||||
|
|||||||
@@ -107,13 +107,14 @@ class PhoneAuthHelper {
|
|||||||
|
|
||||||
/// Verifies the OTP and logs the user in.
|
/// 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 {
|
try {
|
||||||
final fixedPhone = formatSyrianPhone(phoneNumber);
|
final fixedPhone = formatSyrianPhone(phoneNumber);
|
||||||
final response = await CRUD().post(
|
final response = await CRUD().post(
|
||||||
link: _verifyOtpUrl,
|
link: _verifyOtpUrl,
|
||||||
payload: {
|
payload: {
|
||||||
'phone_number': fixedPhone,
|
'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/map_page_passenger.dart';
|
||||||
import '../../views/home/profile/promos_passenger_page.dart';
|
import '../../views/home/profile/promos_passenger_page.dart';
|
||||||
import '../auth/google_sign.dart';
|
import '../auth/google_sign.dart';
|
||||||
import '../functions/audio_record1.dart';
|
import 'package:Intaleq/controller/voice_call_controller.dart';
|
||||||
import '../home/map_passenger_controller.dart';
|
import '../home/map/ride_lifecycle_controller.dart';
|
||||||
|
import '../home/map/ride_state.dart';
|
||||||
import 'local_notification.dart';
|
import 'local_notification.dart';
|
||||||
|
|
||||||
class FirebaseMessagesController extends GetxController {
|
class FirebaseMessagesController extends GetxController {
|
||||||
@@ -105,8 +106,8 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
// اقرأ "النوع" من حمولة البيانات، وليس من العنوان
|
// اقرأ "النوع" من حمولة البيانات، وليس من العنوان
|
||||||
String category = message.data['category'] ?? '';
|
String category = message.data['category'] ?? '';
|
||||||
|
|
||||||
final mapCtrl = Get.isRegistered<MapPassengerController>()
|
final mapCtrl = Get.isRegistered<RideLifecycleController>()
|
||||||
? Get.find<MapPassengerController>()
|
? Get.find<RideLifecycleController>()
|
||||||
: null;
|
: null;
|
||||||
// اقرأ العنوان (للعرض)
|
// اقرأ العنوان (للعرض)
|
||||||
String title = message.data['title'] ?? message.notification?.title ?? '';
|
String title = message.data['title'] ?? message.notification?.title ?? '';
|
||||||
@@ -167,8 +168,8 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
GoogleSignInHelper.signOut();
|
GoogleSignInHelper.signOut();
|
||||||
} else if (category == 'Driver Is Going To Passenger') {
|
} else if (category == 'Driver Is Going To Passenger') {
|
||||||
// <-- كان 'Driver Is Going To Passenger'
|
// <-- كان 'Driver Is Going To Passenger'
|
||||||
Get.find<MapPassengerController>().isDriverInPassengerWay = true;
|
Get.find<RideLifecycleController>().isDriverInPassengerWay = true;
|
||||||
Get.find<MapPassengerController>().update();
|
Get.find<RideLifecycleController>().update();
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
notificationController.showNotification(title, body, 'tone1');
|
notificationController.showNotification(title, body, 'tone1');
|
||||||
}
|
}
|
||||||
@@ -214,7 +215,7 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (driverList.isNotEmpty) {
|
if (driverList.isNotEmpty) {
|
||||||
Get.find<MapPassengerController>()
|
Get.find<RideLifecycleController>()
|
||||||
.processRideFinished(driverList, source: "FCM");
|
.processRideFinished(driverList, source: "FCM");
|
||||||
}
|
}
|
||||||
} else if (category == 'Finish Monitor') {
|
} else if (category == 'Finish Monitor') {
|
||||||
@@ -232,9 +233,9 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
Log.print("🔔 FCM: Ride Cancelled by Driver received.");
|
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");
|
.processRideCancelledByDriver(message.data, source: "FCM");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,6 +248,19 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
// ... (باقي الحالات مثل Call Income, Call End, إلخ) ...
|
// ... (باقي الحالات مثل 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') {
|
else if (category == 'Order Applied') {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
notificationController.showNotification(
|
notificationController.showNotification(
|
||||||
@@ -277,7 +291,7 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
// var myList = jsonDecode(driverListJson) as List<dynamic>;
|
// var myList = jsonDecode(driverListJson) as List<dynamic>;
|
||||||
// Log.print('myList: ${myList}');
|
// Log.print('myList: ${myList}');
|
||||||
|
|
||||||
// final controller = Get.find<MapPassengerController>();
|
// final controller = Get.find<RideLifecycleController>();
|
||||||
|
|
||||||
// // استدعاء الدالة الموحدة الجديدة التي أنشأناها
|
// // استدعاء الدالة الموحدة الجديدة التي أنشأناها
|
||||||
// await controller.processRideAcceptance(
|
// await controller.processRideAcceptance(
|
||||||
@@ -311,8 +325,8 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
// }
|
// }
|
||||||
// GoogleSignInHelper.signOut();
|
// GoogleSignInHelper.signOut();
|
||||||
// } else if (message.notification!.title! == 'Driver Is Going To Passenger') {
|
// } else if (message.notification!.title! == 'Driver Is Going To Passenger') {
|
||||||
// Get.find<MapPassengerController>().isDriverInPassengerWay = true;
|
// Get.find<RideLifecycleController>().isDriverInPassengerWay = true;
|
||||||
// Get.find<MapPassengerController>().update();
|
// Get.find<RideLifecycleController>().update();
|
||||||
// if (Platform.isAndroid) {
|
// if (Platform.isAndroid) {
|
||||||
// notificationController.showNotification('Driver is Going To You'.tr,
|
// notificationController.showNotification('Driver is Going To You'.tr,
|
||||||
// 'Please stay on the picked point.'.tr, 'tone1');
|
// '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();
|
// controller.processRideBegin();
|
||||||
|
|
||||||
// // (تم حذف كل الأوامر التالية من هنا)
|
// // (تم حذف كل الأوامر التالية من هنا)
|
||||||
// // Get.find<MapPassengerController>().getBeginRideFromDriver();
|
// // Get.find<RideLifecycleController>().getBeginRideFromDriver();
|
||||||
// // box.write(BoxName.passengerWalletTotal, '0');
|
// // box.write(BoxName.passengerWalletTotal, '0');
|
||||||
// // update();
|
// // update();
|
||||||
// } else if (message.notification!.title! == 'Hi ,I will go now'.tr) {
|
// } else if (message.notification!.title! == 'Hi ,I will go now'.tr) {
|
||||||
@@ -360,7 +374,7 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
// update();
|
// update();
|
||||||
// } // ... داخل معالج الإشعارات (FCM Handler) ...
|
// } // ... داخل معالج الإشعارات (FCM Handler) ...
|
||||||
// if (message.notification!.title! == 'Hi ,I Arrive your site'.tr) {
|
// if (message.notification!.title! == 'Hi ,I Arrive your site'.tr) {
|
||||||
// final controller = Get.find<MapPassengerController>();
|
// final controller = Get.find<RideLifecycleController>();
|
||||||
|
|
||||||
// // 1. التأكد أننا في الحالة الصحيحة (السائق كان في الطريق)
|
// // 1. التأكد أننا في الحالة الصحيحة (السائق كان في الطريق)
|
||||||
// if (controller.currentRideState.value == RideState.driverApplied) {
|
// if (controller.currentRideState.value == RideState.driverApplied) {
|
||||||
@@ -383,7 +397,7 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
// title: 'Ok'.tr,
|
// title: 'Ok'.tr,
|
||||||
// onPressed: () async {
|
// onPressed: () async {
|
||||||
// Get.back();
|
// Get.back();
|
||||||
// await Get.find<MapPassengerController>()
|
// await Get.find<RideLifecycleController>()
|
||||||
// .reSearchAfterCanceledFromDriver();
|
// .reSearchAfterCanceledFromDriver();
|
||||||
// },
|
// },
|
||||||
// ),
|
// ),
|
||||||
@@ -394,7 +408,7 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
// Get.offAll(() => const MapPagePassenger());
|
// Get.offAll(() => const MapPagePassenger());
|
||||||
// },
|
// },
|
||||||
// )
|
// )
|
||||||
// // Get.find<MapPassengerController>()
|
// // Get.find<RideLifecycleController>()
|
||||||
// // .searchNewDriverAfterRejectingFromDriver();
|
// // .searchNewDriverAfterRejectingFromDriver();
|
||||||
// );
|
// );
|
||||||
// } else if (message.notification!.title! == 'Driver Finish Trip'.tr) {
|
// } else if (message.notification!.title! == 'Driver Finish Trip'.tr) {
|
||||||
@@ -434,7 +448,7 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
// box.write(BoxName.passengerWalletTotal, 0);
|
// box.write(BoxName.passengerWalletTotal, 0);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Get.find<MapPassengerController>().tripFinishedFromDriver();
|
// Get.find<RideLifecycleController>().tripFinishedFromDriver();
|
||||||
|
|
||||||
// NotificationController().showNotification(
|
// NotificationController().showNotification(
|
||||||
// 'Don’t forget your personal belongings.'.tr,
|
// 'Don’t forget your personal belongings.'.tr,
|
||||||
@@ -535,7 +549,7 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
// box.write(BoxName.parentTripSelected, false);
|
// box.write(BoxName.parentTripSelected, false);
|
||||||
// box.remove(BoxName.tokenParent);
|
// box.remove(BoxName.tokenParent);
|
||||||
|
|
||||||
// Get.find<MapPassengerController>().restCounter();
|
// Get.find<RideLifecycleController>().restCounter();
|
||||||
// Get.offAll(() => const MapPagePassenger());
|
// Get.offAll(() => const MapPagePassenger());
|
||||||
// }
|
// }
|
||||||
// // else if (message.notification!.title! == 'Order Applied') {
|
// // else if (message.notification!.title! == 'Order Applied') {
|
||||||
@@ -595,8 +609,8 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
// Get.find<FirebaseMessagesController>().sendNotificationToPassengerToken(
|
// Get.find<FirebaseMessagesController>().sendNotificationToPassengerToken(
|
||||||
// 'Hi ,I will go now'.tr,
|
// 'Hi ,I will go now'.tr,
|
||||||
// 'I will go now'.tr,
|
// 'I will go now'.tr,
|
||||||
// Get.find<MapPassengerController>().driverToken, []);
|
// Get.find<RideLifecycleController>().driverToken, []);
|
||||||
// Get.find<MapPassengerController>()
|
// Get.find<RideLifecycleController>()
|
||||||
// .startTimerDriverWaitPassenger5Minute();
|
// .startTimerDriverWaitPassenger5Minute();
|
||||||
|
|
||||||
Get.back();
|
Get.back();
|
||||||
@@ -639,12 +653,12 @@ class DriverTipWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
// Text(
|
// Text(
|
||||||
// '${'Your fee is '.tr}${Get.find<MapPassengerController>().totalPassenger.toStringAsFixed(2)}'),
|
// '${'Your fee is '.tr}${Get.find<RideLifecycleController>().totalPassenger.toStringAsFixed(2)}'),
|
||||||
Text(
|
Text(
|
||||||
'Do you want to pay Tips for this Driver'.tr,
|
'Do you want to pay Tips for this Driver'.tr,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class AudioRecorderController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start recording
|
// Start recording
|
||||||
Future<void> startRecording() async {
|
Future<void> startRecording({String? rideId}) async {
|
||||||
final bool isPermissionGranted = await recorder.hasPermission();
|
final bool isPermissionGranted = await recorder.hasPermission();
|
||||||
if (!isPermissionGranted) {
|
if (!isPermissionGranted) {
|
||||||
// RecordingPermissionException('l');
|
// RecordingPermissionException('l');
|
||||||
@@ -38,10 +38,12 @@ class AudioRecorderController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final directory = await getApplicationDocumentsDirectory();
|
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
|
// Generate a unique file name using the current timestamp
|
||||||
String fileName =
|
String fileName = (rideId != null && rideId.isNotEmpty && rideId != 'yet' && rideId != 'null')
|
||||||
// '${DateTime.now().year}-${DateTime.now().month}-${DateTime.now().day}_${Get.find<MapPassengerController>().rideId}.m4a';
|
? '${dateStr}_$rideId.m4a'
|
||||||
'${DateTime.now().year}-${DateTime.now().month}-${DateTime.now().day}.m4a';
|
: '$dateStr.m4a';
|
||||||
filePath = '${directory.path}/$fileName';
|
filePath = '${directory.path}/$fileName';
|
||||||
|
|
||||||
// Define the configuration for the recording
|
// 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 'package:Intaleq/views/widgets/my_textField.dart';
|
||||||
|
|
||||||
import '../../constant/style.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 {
|
class LogOutController extends GetxController {
|
||||||
TextEditingController checkTxtController = TextEditingController();
|
TextEditingController checkTxtController = TextEditingController();
|
||||||
@@ -110,6 +118,15 @@ class LogOutController extends GetxController {
|
|||||||
box.remove(BoxName.accountIdStripeConnect);
|
box.remove(BoxName.accountIdStripeConnect);
|
||||||
box.remove(BoxName.passengerWalletTotal);
|
box.remove(BoxName.passengerWalletTotal);
|
||||||
box.remove(BoxName.isVerified);
|
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());
|
Get.offAll(OnBoardingPage());
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
@@ -76,9 +76,11 @@ class ImageController extends GetxController {
|
|||||||
length,
|
length,
|
||||||
filename: basename(file.path),
|
filename: basename(file.path),
|
||||||
);
|
);
|
||||||
|
final String fingerPrint = box.read(BoxName.deviceFpEncrypted)?.toString() ?? '';
|
||||||
request.headers.addAll({
|
request.headers.addAll({
|
||||||
'Authorization':
|
'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
|
// Set the file name to the driverID
|
||||||
request.files.add(
|
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:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||||
import 'package:Intaleq/constant/style.dart';
|
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
|
||||||
|
|
||||||
import '../../constant/api_key.dart';
|
import '../../constant/api_key.dart';
|
||||||
import '../../constant/links.dart';
|
import '../../constant/links.dart';
|
||||||
|
import '../../constant/style.dart';
|
||||||
import '../functions/crud.dart';
|
import '../functions/crud.dart';
|
||||||
import '../functions/location_controller.dart';
|
import '../functions/location_controller.dart';
|
||||||
|
|
||||||
@@ -114,14 +114,21 @@ class WayPointController extends GetxController {
|
|||||||
void onInit() {
|
void onInit() {
|
||||||
// Get.put(LocationController());
|
// Get.put(LocationController());
|
||||||
addWayPoints();
|
addWayPoints();
|
||||||
myLocation = Get.find<MapPassengerController>().passengerLocation;
|
myLocation = Get.find<RideLifecycleController>().passengerLocation;
|
||||||
super.onInit();
|
super.onInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
wayPoints.clear();
|
||||||
|
addWayPoints();
|
||||||
|
placeListResponse.clear();
|
||||||
|
update();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlaceList extends StatelessWidget {
|
class PlaceList extends StatelessWidget {
|
||||||
// Get the controller instance
|
// Get the controller instance
|
||||||
final controller = Get.put(WayPointController());
|
final controller = Get.find<WayPointController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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 uri = Uri.parse('${AppLink.server}/upload_audio.php');
|
||||||
var request = http.MultipartRequest('POST', uri);
|
var request = http.MultipartRequest('POST', uri);
|
||||||
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
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);
|
var mimeType = lookupMimeType(audioFile.path);
|
||||||
// ** التعديل: تم استخدام نفس هيدر التوثيق **
|
// ** التعديل: تم استخدام نفس هيدر التوثيق **
|
||||||
request.headers.addAll({
|
request.headers.addAll({
|
||||||
'Authorization': 'Bearer $token',
|
'Authorization': 'Bearer $token',
|
||||||
|
'X-Device-FP': fingerPrint,
|
||||||
});
|
});
|
||||||
request.files.add(
|
request.files.add(
|
||||||
await http.MultipartFile.fromPath(
|
await http.MultipartFile.fromPath(
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import 'dart:convert';
|
|||||||
import 'package:Intaleq/constant/box_name.dart';
|
import 'package:Intaleq/constant/box_name.dart';
|
||||||
import 'package:Intaleq/constant/colors.dart';
|
import 'package:Intaleq/constant/colors.dart';
|
||||||
import 'package:Intaleq/constant/links.dart';
|
import 'package:Intaleq/constant/links.dart';
|
||||||
import 'package:Intaleq/constant/style.dart';
|
import 'package:Intaleq/controller/home/map/ride_lifecycle_controller.dart';
|
||||||
import 'package:Intaleq/controller/home/map_passenger_controller.dart';
|
|
||||||
import 'package:Intaleq/main.dart';
|
import 'package:Intaleq/main.dart';
|
||||||
import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
||||||
import 'package:Intaleq/views/widgets/my_scafold.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:get/get.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import '../../constant/style.dart';
|
||||||
import '../functions/crud.dart';
|
import '../functions/crud.dart';
|
||||||
import '../functions/encrypt_decrypt.dart';
|
import '../functions/encrypt_decrypt.dart';
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class VipOrderController extends GetxController {
|
|||||||
Future<void> fetchOrder() async {
|
Future<void> fetchOrder() async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
var mapPassengerController = Get.find<MapPassengerController>();
|
var mapPassengerController = Get.find<RideLifecycleController>();
|
||||||
|
|
||||||
var res = await CRUD().get(
|
var res = await CRUD().get(
|
||||||
link: AppLink.getMishwari,
|
link: AppLink.getMishwari,
|
||||||
@@ -239,7 +239,7 @@ class VipWaittingPage extends StatelessWidget {
|
|||||||
title: "Cancel Trip".tr,
|
title: "Cancel Trip".tr,
|
||||||
kolor: AppColor.redColor,
|
kolor: AppColor.redColor,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Get.find<MapPassengerController>().cancelVip(
|
Get.find<RideLifecycleController>().cancelVip(
|
||||||
data['token'].toString(),
|
data['token'].toString(),
|
||||||
data['id'].toString(),
|
data['id'].toString(),
|
||||||
);
|
);
|
||||||
@@ -272,7 +272,7 @@ class VipWaittingPage extends StatelessWidget {
|
|||||||
title: "Send to Driver Again".tr,
|
title: "Send to Driver Again".tr,
|
||||||
kolor: AppColor.greenColor,
|
kolor: AppColor.greenColor,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Get.find<MapPassengerController>()
|
Get.find<RideLifecycleController>()
|
||||||
.sendToDriverAgain(data['token']);
|
.sendToDriverAgain(data['token']);
|
||||||
vipOrderController.fetchOrder();
|
vipOrderController.fetchOrder();
|
||||||
},
|
},
|
||||||
@@ -292,7 +292,7 @@ class VipWaittingPage extends StatelessWidget {
|
|||||||
kolor: AppColor.greenColor,
|
kolor: AppColor.greenColor,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final mapPassengerController =
|
final mapPassengerController =
|
||||||
Get.find<MapPassengerController>();
|
Get.find<RideLifecycleController>();
|
||||||
mapPassengerController.make = data['make'];
|
mapPassengerController.make = data['make'];
|
||||||
mapPassengerController.licensePlate =
|
mapPassengerController.licensePlate =
|
||||||
data['car_plate'];
|
data['car_plate'];
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class MyTranslation extends Translations {
|
|||||||
"4 Passengers": "٤ ركاب",
|
"4 Passengers": "٤ ركاب",
|
||||||
"2. Attach Recorded Audio (Optional)":
|
"2. Attach Recorded Audio (Optional)":
|
||||||
"٢. إرفاق تسجيل صوتي (اختياري)",
|
"٢. إرفاق تسجيل صوتي (اختياري)",
|
||||||
|
"Accept": "قبول",
|
||||||
"Account": "الحساب",
|
"Account": "الحساب",
|
||||||
"Actions": "الإجراءات",
|
"Actions": "الإجراءات",
|
||||||
"Active Users": "المستخدمين النشطين",
|
"Active Users": "المستخدمين النشطين",
|
||||||
@@ -41,9 +42,12 @@ class MyTranslation extends Translations {
|
|||||||
"Arrived": "وصلنا",
|
"Arrived": "وصلنا",
|
||||||
"Audio Recording": "تسجيل صوتي",
|
"Audio Recording": "تسجيل صوتي",
|
||||||
"Call": "اتصال",
|
"Call": "اتصال",
|
||||||
|
"Call Connected": "تم فتح الاتصال",
|
||||||
"Call Support": "اتصل بالدعم",
|
"Call Support": "اتصل بالدعم",
|
||||||
"Call left": "مكالمات متبقية",
|
"Call left": "مكالمات متبقية",
|
||||||
|
"Calling": "عم نتصل بـ",
|
||||||
"Change Photo": "تغيير الصورة",
|
"Change Photo": "تغيير الصورة",
|
||||||
|
"Captain": "الكابتن",
|
||||||
"Choose from Gallery": "اختر من المعرض",
|
"Choose from Gallery": "اختر من المعرض",
|
||||||
"Choose from contact": "اختر من جهات الاتصال",
|
"Choose from contact": "اختر من جهات الاتصال",
|
||||||
"Click to track the trip": "اضغط لتتبع المشوار",
|
"Click to track the trip": "اضغط لتتبع المشوار",
|
||||||
@@ -52,11 +56,13 @@ class MyTranslation extends Translations {
|
|||||||
"Complete Payment": "إتمام الدفع",
|
"Complete Payment": "إتمام الدفع",
|
||||||
"Confirm Cancellation": "تأكيد الإلغاء",
|
"Confirm Cancellation": "تأكيد الإلغاء",
|
||||||
"Confirm Pickup Location": "تأكيد موقع الانطلاق",
|
"Confirm Pickup Location": "تأكيد موقع الانطلاق",
|
||||||
|
"Connecting...": "عم يتم الاتصال...",
|
||||||
"Connection failed. Please try again.": "فشل الاتصال. جرب مرة تانية.",
|
"Connection failed. Please try again.": "فشل الاتصال. جرب مرة تانية.",
|
||||||
"Could not create ride. Please try again.":
|
"Could not create ride. Please try again.":
|
||||||
"ما قدرنا نعمل مشوار. جرب مرة تانية.",
|
"ما قدرنا نعمل مشوار. جرب مرة تانية.",
|
||||||
"Crop Photo": "قص الصورة",
|
"Crop Photo": "قص الصورة",
|
||||||
"Dark Mode": "الوضع الليلي",
|
"Dark Mode": "الوضع الليلي",
|
||||||
|
"Decline": "رفض",
|
||||||
"Delete": "حذف",
|
"Delete": "حذف",
|
||||||
"Delete Account": "حذف الحساب",
|
"Delete Account": "حذف الحساب",
|
||||||
"Delete All": "حذف الكل",
|
"Delete All": "حذف الكل",
|
||||||
@@ -73,6 +79,7 @@ class MyTranslation extends Translations {
|
|||||||
"Driver is Going To You": "الكابتن جاي لعندك",
|
"Driver is Going To You": "الكابتن جاي لعندك",
|
||||||
"Emergency Mode Triggered": "تم تفعيل وضع الطوارئ",
|
"Emergency Mode Triggered": "تم تفعيل وضع الطوارئ",
|
||||||
"Emergency SOS": "طوارئ SOS",
|
"Emergency SOS": "طوارئ SOS",
|
||||||
|
"End": "إنهاء",
|
||||||
"Enter the 5-digit code": "أدخل الكود المكون من ٥ أرقام",
|
"Enter the 5-digit code": "أدخل الكود المكون من ٥ أرقام",
|
||||||
"Enter your City": "أدخل مدينتك",
|
"Enter your City": "أدخل مدينتك",
|
||||||
"Enter your Password": "أدخل كلمة السر",
|
"Enter your Password": "أدخل كلمة السر",
|
||||||
@@ -84,6 +91,7 @@ class MyTranslation extends Translations {
|
|||||||
"Failed to upload photo": "فشل رفع الصورة",
|
"Failed to upload photo": "فشل رفع الصورة",
|
||||||
"Finished": "انتهى",
|
"Finished": "انتهى",
|
||||||
"Fixed Price": "سعر ثابت",
|
"Fixed Price": "سعر ثابت",
|
||||||
|
"Free Call": "مكالمة مجانية",
|
||||||
"General": "عام",
|
"General": "عام",
|
||||||
"Grant": "منح الإذن",
|
"Grant": "منح الإذن",
|
||||||
"Have a Promo Code?": "معك كود خصم؟",
|
"Have a Promo Code?": "معك كود خصم؟",
|
||||||
@@ -109,6 +117,7 @@ class MyTranslation extends Translations {
|
|||||||
"Logout": "تسجيل خروج",
|
"Logout": "تسجيل خروج",
|
||||||
"Map Error": "خطأ بالخريطة",
|
"Map Error": "خطأ بالخريطة",
|
||||||
"Message": "رسالة",
|
"Message": "رسالة",
|
||||||
|
"Mute": "كتم الصوت",
|
||||||
"Move map to select destination": "حرك الخريطة لتحديد الوجهة",
|
"Move map to select destination": "حرك الخريطة لتحديد الوجهة",
|
||||||
"Move map to set start location": "حرك الخريطة لتحديد نقطة البداية",
|
"Move map to set start location": "حرك الخريطة لتحديد نقطة البداية",
|
||||||
"Move map to set stop": "حرك الخريطة لتحديد التوقف",
|
"Move map to set stop": "حرك الخريطة لتحديد التوقف",
|
||||||
@@ -205,6 +214,7 @@ class MyTranslation extends Translations {
|
|||||||
"Share your experience to help us improve...":
|
"Share your experience to help us improve...":
|
||||||
"شاركنا تجربتك لنحسن خدمتنا...",
|
"شاركنا تجربتك لنحسن خدمتنا...",
|
||||||
"Something went wrong. Please try again.": "صار غلط. جرب مرة تانية.",
|
"Something went wrong. Please try again.": "صار غلط. جرب مرة تانية.",
|
||||||
|
"Speaker": "مكبر الصوت",
|
||||||
"Speaking...": "عم يحكي...",
|
"Speaking...": "عم يحكي...",
|
||||||
"Speed Over": "تجاوز السرعة",
|
"Speed Over": "تجاوز السرعة",
|
||||||
"Start Point": "نقطة البداية",
|
"Start Point": "نقطة البداية",
|
||||||
@@ -254,6 +264,7 @@ class MyTranslation extends Translations {
|
|||||||
"cancelled": "تكنسل",
|
"cancelled": "تكنسل",
|
||||||
"due to a previous trip.": "عن مشوار قديم.",
|
"due to a previous trip.": "عن مشوار قديم.",
|
||||||
"insert sos phone": "دخل رقم طوارئ",
|
"insert sos phone": "دخل رقم طوارئ",
|
||||||
|
"is calling you": "عم يتصل فيك",
|
||||||
"is driving a": "عم يسوق",
|
"is driving a": "عم يسوق",
|
||||||
"min added to fare": "دقيقة نضافت للأجرة",
|
"min added to fare": "دقيقة نضافت للأجرة",
|
||||||
"phone not verified": "رقم الموبايل مو متأكد",
|
"phone not verified": "رقم الموبايل مو متأكد",
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import 'package:flutter_paypal/flutter_paypal.dart';
|
|||||||
import 'package:flutter_stripe/flutter_stripe.dart';
|
import 'package:flutter_stripe/flutter_stripe.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:local_auth/local_auth.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 'package:webview_flutter/webview_flutter.dart';
|
||||||
|
|
||||||
import '../../constant/box_name.dart';
|
import '../../constant/box_name.dart';
|
||||||
@@ -34,7 +36,7 @@ class PaymentController extends GetxController {
|
|||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
final promo = TextEditingController();
|
final promo = TextEditingController();
|
||||||
final walletphoneController = TextEditingController();
|
final walletphoneController = TextEditingController();
|
||||||
double totalPassenger = Get.find<MapPassengerController>().totalPassenger;
|
double totalPassenger = Get.find<RideLifecycleController>().totalPassenger;
|
||||||
int? selectedAmount = 0;
|
int? selectedAmount = 0;
|
||||||
List<dynamic> totalPassengerWalletDetails = [];
|
List<dynamic> totalPassengerWalletDetails = [];
|
||||||
String passengerTotalWalletAmount = '';
|
String passengerTotalWalletAmount = '';
|
||||||
@@ -79,7 +81,7 @@ class PaymentController extends GetxController {
|
|||||||
|
|
||||||
Future<String> generateTokenDriver(String amount) async {
|
Future<String> generateTokenDriver(String amount) async {
|
||||||
var res = await CRUD().post(link: AppLink.addPaymentTokenDriver, payload: {
|
var res = await CRUD().post(link: AppLink.addPaymentTokenDriver, payload: {
|
||||||
'driverID': Get.find<MapPassengerController>().driverId,
|
'driverID': Get.find<RideLifecycleController>().driverId,
|
||||||
'amount': amount.toString(),
|
'amount': amount.toString(),
|
||||||
});
|
});
|
||||||
var d = jsonDecode(res);
|
var d = jsonDecode(res);
|
||||||
@@ -121,7 +123,7 @@ class PaymentController extends GetxController {
|
|||||||
'payment_method': 'cancel-from-near',
|
'payment_method': 'cancel-from-near',
|
||||||
'passengerID': box.read(BoxName.passengerID).toString(),
|
'passengerID': box.read(BoxName.passengerID).toString(),
|
||||||
'token': paymentTokenWait,
|
'token': paymentTokenWait,
|
||||||
'driverID': Get.find<MapPassengerController>().driverId.toString(),
|
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
|
||||||
});
|
});
|
||||||
var paymentTokenWait1 =
|
var paymentTokenWait1 =
|
||||||
await generateTokenDriver(costOfWaiting5Minute.toString());
|
await generateTokenDriver(costOfWaiting5Minute.toString());
|
||||||
@@ -131,7 +133,7 @@ class PaymentController extends GetxController {
|
|||||||
'amount': (costOfWaiting5Minute).toStringAsFixed(0),
|
'amount': (costOfWaiting5Minute).toStringAsFixed(0),
|
||||||
'paymentMethod': 'cancel-from-near',
|
'paymentMethod': 'cancel-from-near',
|
||||||
'token': paymentTokenWait1,
|
'token': paymentTokenWait1,
|
||||||
'driverID': Get.find<MapPassengerController>().driverId.toString(),
|
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
@@ -139,13 +141,13 @@ class PaymentController extends GetxController {
|
|||||||
// 'Cancel',
|
// 'Cancel',
|
||||||
// 'Trip Cancelled. The cost of the trip will be added to your wallet.'
|
// 'Trip Cancelled. The cost of the trip will be added to your wallet.'
|
||||||
// .tr,
|
// .tr,
|
||||||
// Get.find<MapPassengerController>().driverToken,
|
// Get.find<RideLifecycleController>().driverToken,
|
||||||
// [],
|
// [],
|
||||||
// 'cancel',
|
// 'cancel',
|
||||||
// );
|
// );
|
||||||
await NotificationService.sendNotification(
|
await NotificationService.sendNotification(
|
||||||
category: 'Cancel',
|
category: 'Cancel',
|
||||||
target: Get.find<MapPassengerController>().driverToken.toString(),
|
target: Get.find<RideLifecycleController>().driverToken.toString(),
|
||||||
title: 'Cancel'.tr,
|
title: 'Cancel'.tr,
|
||||||
body:
|
body:
|
||||||
'Trip Cancelled. The cost of the trip will be added to your wallet.'
|
'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];
|
var firstElement = decod["message"][0];
|
||||||
totalPassenger = totalPassenger -
|
totalPassenger = totalPassenger -
|
||||||
(totalPassenger * int.parse(firstElement['amount']));
|
(totalPassenger * int.parse(firstElement['amount']));
|
||||||
MapPassengerController().promoTaken = true;
|
Get.find<RideLifecycleController>().promoTaken = true;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:Intaleq/constant/box_name.dart';
|
|||||||
import 'package:Intaleq/constant/links.dart';
|
import 'package:Intaleq/constant/links.dart';
|
||||||
import 'package:Intaleq/constant/style.dart';
|
import 'package:Intaleq/constant/style.dart';
|
||||||
import 'package:Intaleq/controller/functions/crud.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/main.dart';
|
||||||
import 'package:Intaleq/views/home/map_page_passenger.dart';
|
import 'package:Intaleq/views/home/map_page_passenger.dart';
|
||||||
import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
||||||
@@ -45,14 +45,14 @@ class RateController extends GetxController {
|
|||||||
confirm: MyElevatedButton(title: 'Ok', onPressed: () => Get.back()));
|
confirm: MyElevatedButton(title: 'Ok', onPressed: () => Get.back()));
|
||||||
} else if (Get.find<PaymentController>().isWalletChecked == true) {
|
} else if (Get.find<PaymentController>().isWalletChecked == true) {
|
||||||
double tip = 0;
|
double tip = 0;
|
||||||
tip = (Get.find<MapPassengerController>().totalPassenger) *
|
tip = (Get.find<RideLifecycleController>().totalPassenger) *
|
||||||
(double.parse(box.read(BoxName.tipPercentage).toString()));
|
(double.parse(box.read(BoxName.tipPercentage).toString()));
|
||||||
|
|
||||||
if (tip > 0) {
|
if (tip > 0) {
|
||||||
var res = await CRUD().post(link: AppLink.addTips, payload: {
|
var res = await CRUD().post(link: AppLink.addTips, payload: {
|
||||||
'passengerID': box.read(BoxName.passengerID),
|
'passengerID': box.read(BoxName.passengerID),
|
||||||
'driverID': Get.find<MapPassengerController>().driverId.toString(),
|
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
|
||||||
'rideID': Get.find<MapPassengerController>().rideId.toString(),
|
'rideID': Get.find<RideLifecycleController>().rideId.toString(),
|
||||||
'tipAmount': tip.toString(),
|
'tipAmount': tip.toString(),
|
||||||
});
|
});
|
||||||
await Get.find<PaymentController>()
|
await Get.find<PaymentController>()
|
||||||
@@ -62,8 +62,8 @@ class RateController extends GetxController {
|
|||||||
? tip.toStringAsFixed(0)
|
? tip.toStringAsFixed(0)
|
||||||
: (tip * 100).toString());
|
: (tip * 100).toString());
|
||||||
await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: {
|
await CRUD().postWallet(link: AppLink.addDriversWalletPoints, payload: {
|
||||||
'driverID': Get.find<MapPassengerController>().driverId.toString(),
|
'driverID': Get.find<RideLifecycleController>().driverId.toString(),
|
||||||
'paymentID': '${Get.find<MapPassengerController>().rideId}tip',
|
'paymentID': '${Get.find<RideLifecycleController>().rideId}tip',
|
||||||
'amount': box.read(BoxName.countryCode) == 'Egypt'
|
'amount': box.read(BoxName.countryCode) == 'Egypt'
|
||||||
? tip.toStringAsFixed(0)
|
? tip.toStringAsFixed(0)
|
||||||
: (tip * 100).toString(),
|
: (tip * 100).toString(),
|
||||||
@@ -73,10 +73,10 @@ class RateController extends GetxController {
|
|||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
await NotificationService.sendNotification(
|
await NotificationService.sendNotification(
|
||||||
category: 'You Have Tips',
|
category: 'You Have Tips',
|
||||||
target: Get.find<MapPassengerController>().driverToken.toString(),
|
target: Get.find<RideLifecycleController>().driverToken.toString(),
|
||||||
title: 'You Have Tips'.tr,
|
title: 'You Have Tips'.tr,
|
||||||
body:
|
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
|
isTopic: false, // Important: this is a token
|
||||||
tone: 'ding',
|
tone: 'ding',
|
||||||
driverList: [],
|
driverList: [],
|
||||||
@@ -95,7 +95,7 @@ class RateController extends GetxController {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Get.find<MapPassengerController>().restCounter();
|
Get.find<RideLifecycleController>().restCounter();
|
||||||
Get.offAll(const MapPagePassenger());
|
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/material.dart';
|
||||||
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -40,7 +40,7 @@ class RateDriverFromPassenger extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${'Total price to '.tr}${Get.find<MapPassengerController>().driverName}',
|
'${'Total price to '.tr}${Get.find<RideLifecycleController>().driverName}',
|
||||||
style: AppStyle.title,
|
style: AppStyle.title,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@@ -189,7 +189,7 @@ class RateDriverFromPassenger extends StatelessWidget {
|
|||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color:
|
color:
|
||||||
AppColor.grayColor.withOpacity(0.5)), // Customize the border color
|
AppColor.grayColor.withValues(alpha: 0.5)), // Customize the border color
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
|
|||||||
@@ -410,7 +410,7 @@ class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
|
|||||||
final rawPhone = _phoneController.text.trim().replaceFirst('+', '');
|
final rawPhone = _phoneController.text.trim().replaceFirst('+', '');
|
||||||
final success = await PhoneAuthHelper.sendOtp(rawPhone);
|
final success = await PhoneAuthHelper.sendOtp(rawPhone);
|
||||||
if (success && mounted) {
|
if (success && mounted) {
|
||||||
await PhoneAuthHelper.verifyOtp(rawPhone);
|
Get.to(() => OtpVerificationScreen(phoneNumber: rawPhone));
|
||||||
}
|
}
|
||||||
if (mounted) setState(() => _isLoading = false);
|
if (mounted) setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
@@ -537,7 +537,7 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
|||||||
void _submit() async {
|
void _submit() async {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
// Logic for OTP verification here
|
await PhoneAuthHelper.verifyOtp(widget.phoneNumber, _otpController.text.trim());
|
||||||
if (mounted) setState(() => _isLoading = false);
|
if (mounted) setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -596,7 +596,7 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
|||||||
controller: _otpController,
|
controller: _otpController,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
maxLength: 5,
|
maxLength: 3,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 30,
|
fontSize: 30,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
@@ -605,7 +605,7 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
|||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
counterText: '',
|
counterText: '',
|
||||||
hintText: '·····',
|
hintText: '···',
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: isDark ? Colors.white12 : const Color(0xFFD1D5DB),
|
color: isDark ? Colors.white12 : const Color(0xFFD1D5DB),
|
||||||
letterSpacing: 20,
|
letterSpacing: 20,
|
||||||
@@ -615,7 +615,7 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
|||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 6),
|
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>(
|
ValueListenableBuilder<TextEditingValue>(
|
||||||
valueListenable: _otpController,
|
valueListenable: _otpController,
|
||||||
builder: (_, value, __) {
|
builder: (_, value, __) {
|
||||||
final filled = value.text.length.clamp(0, 5);
|
final filled = value.text.length.clamp(0, 3);
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: List.generate(5, (i) {
|
children: List.generate(3, (i) {
|
||||||
final active = i < filled;
|
final active = i < filled;
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ class OtpVerificationPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _OtpVerificationPageState extends State<OtpVerificationPage> {
|
class _OtpVerificationPageState extends State<OtpVerificationPage> {
|
||||||
late final OtpVerificationController controller;
|
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 =
|
final List<TextEditingController> _textControllers =
|
||||||
List.generate(5, (index) => TextEditingController());
|
List.generate(3, (index) => TextEditingController());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -50,7 +50,7 @@ class _OtpVerificationPageState extends State<OtpVerificationPage> {
|
|||||||
|
|
||||||
void _onOtpChanged(String value, int index) {
|
void _onOtpChanged(String value, int index) {
|
||||||
if (value.isNotEmpty) {
|
if (value.isNotEmpty) {
|
||||||
if (index < 5) {
|
if (index < 2) {
|
||||||
_focusNodes[index + 1].requestFocus();
|
_focusNodes[index + 1].requestFocus();
|
||||||
} else {
|
} else {
|
||||||
_focusNodes[index].unfocus(); // إلغاء التركيز بعد آخر حقل
|
_focusNodes[index].unfocus(); // إلغاء التركيز بعد آخر حقل
|
||||||
@@ -67,7 +67,7 @@ class _OtpVerificationPageState extends State<OtpVerificationPage> {
|
|||||||
textDirection: TextDirection.ltr, // لضمان ترتيب الحقول من اليسار لليمين
|
textDirection: TextDirection.ltr, // لضمان ترتيب الحقول من اليسار لليمين
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: List.generate(5, (index) {
|
children: List.generate(3, (index) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: 45,
|
width: 45,
|
||||||
height: 55,
|
height: 55,
|
||||||
|
|||||||
@@ -3,8 +3,13 @@ import 'package:get/get.dart';
|
|||||||
import '../../constant/box_name.dart';
|
import '../../constant/box_name.dart';
|
||||||
import '../../constant/colors.dart';
|
import '../../constant/colors.dart';
|
||||||
import '../../controller/functions/crud.dart';
|
import '../../controller/functions/crud.dart';
|
||||||
import '../../controller/functions/package_info.dart';
|
import '../../controller/home/map/map_socket_controller.dart';
|
||||||
import '../../controller/home/map_passenger_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 '../../main.dart';
|
||||||
import '../../views/home/map_widget.dart/ride_begin_passenger.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/left_main_menu_icons.dart';
|
||||||
import 'map_widget.dart/main_bottom_menu_map.dart';
|
import 'map_widget.dart/main_bottom_menu_map.dart';
|
||||||
import 'map_widget.dart/map_menu_widget.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/passengerRideLoctionWidget.dart';
|
||||||
import 'map_widget.dart/payment_method.page.dart';
|
import 'map_widget.dart/payment_method.page.dart';
|
||||||
import 'map_widget.dart/points_page_for_rider.dart';
|
import 'map_widget.dart/points_page_for_rider.dart';
|
||||||
@@ -30,9 +35,14 @@ class MapPagePassenger extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(MapPassengerController());
|
Get.find<MapSocketController>();
|
||||||
Get.put(MyMenuController());
|
Get.find<MapEngineController>();
|
||||||
Get.put(CRUD());
|
Get.find<LocationSearchController>();
|
||||||
|
Get.find<NearbyDriversController>();
|
||||||
|
Get.find<RideLifecycleController>();
|
||||||
|
Get.find<UiInteractionsController>();
|
||||||
|
Get.find<MyMenuController>();
|
||||||
|
Get.find<CRUD>();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
checkForUpdate(context);
|
checkForUpdate(context);
|
||||||
});
|
});
|
||||||
@@ -118,7 +128,7 @@ class CancelRidePageShow extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<RideLifecycleController>(
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
// نستخدم RideState Enum لأنه أدق، أو نصلح المنطق النصي
|
// نستخدم RideState Enum لأنه أدق، أو نصلح المنطق النصي
|
||||||
// الشرط:
|
// الشرط:
|
||||||
@@ -175,7 +185,7 @@ class PickerIconOnMap extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<RideLifecycleController>(
|
||||||
builder: (controller) => controller.isPickerShown
|
builder: (controller) => controller.isPickerShown
|
||||||
? Positioned(
|
? Positioned(
|
||||||
bottom: Get.height * .2,
|
bottom: Get.height * .2,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:Intaleq/constant/colors.dart';
|
import 'package:Intaleq/constant/colors.dart';
|
||||||
import 'package:Intaleq/constant/links.dart';
|
import 'package:Intaleq/constant/links.dart';
|
||||||
import 'package:Intaleq/constant/style.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/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -10,6 +12,7 @@ import 'package:intl/intl.dart';
|
|||||||
import '../../../constant/box_name.dart';
|
import '../../../constant/box_name.dart';
|
||||||
import '../../../controller/firebase/notification_service.dart';
|
import '../../../controller/firebase/notification_service.dart';
|
||||||
import '../../../controller/functions/launch.dart';
|
import '../../../controller/functions/launch.dart';
|
||||||
|
import '../../../controller/functions/crud.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
|
|
||||||
class ApplyOrderWidget extends StatelessWidget {
|
class ApplyOrderWidget extends StatelessWidget {
|
||||||
@@ -29,7 +32,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final controller = Get.find<MapPassengerController>();
|
final controller = Get.find<RideLifecycleController>();
|
||||||
|
|
||||||
final bool isVisible =
|
final bool isVisible =
|
||||||
controller.currentRideState.value == RideState.driverApplied ||
|
controller.currentRideState.value == RideState.driverApplied ||
|
||||||
@@ -57,7 +60,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
// تغيير: تقليل الحواف الخارجية بشكل كبير
|
// تغيير: تقليل الحواف الخارجية بشكل كبير
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
child: GetBuilder<MapPassengerController>(
|
child: GetBuilder<RideLifecycleController>(
|
||||||
builder: (c) {
|
builder: (c) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize:
|
mainAxisSize:
|
||||||
@@ -106,15 +109,18 @@ class ApplyOrderWidget extends StatelessWidget {
|
|||||||
// [NEW] 1. صف الرأس المضغوط (يحتوي الحالة + الإحصائيات + السعر)
|
// [NEW] 1. صف الرأس المضغوط (يحتوي الحالة + الإحصائيات + السعر)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
Widget _buildCompactHeaderRow(
|
Widget _buildCompactHeaderRow(
|
||||||
BuildContext context, MapPassengerController controller) {
|
BuildContext context, RideLifecycleController controller) {
|
||||||
// تنسيق السعر
|
// تنسيق السعر
|
||||||
final formatter = NumberFormat("#,###");
|
final formatter = NumberFormat("#,###");
|
||||||
String formattedPrice = formatter.format(controller.totalPassenger);
|
String formattedPrice = formatter.format(controller.totalPassenger);
|
||||||
|
|
||||||
// حساب الدقائق
|
// حساب الدقائق من الوقت المتبقي الحي، وليس ETA الأصلي فقط.
|
||||||
int minutes =
|
final int secondsToPassenger =
|
||||||
(controller.timeToPassengerFromDriverAfterApplied / 60).ceil();
|
controller.remainingTimeToPassengerFromDriverAfterApplied > 0
|
||||||
if (minutes < 1) minutes = 1;
|
? controller.remainingTimeToPassengerFromDriverAfterApplied
|
||||||
|
: controller.timeToPassengerFromDriverAfterApplied;
|
||||||
|
final int minutes =
|
||||||
|
secondsToPassenger <= 0 ? 0 : (secondsToPassenger / 60).ceil();
|
||||||
|
|
||||||
// تنسيق المسافة
|
// تنسيق المسافة
|
||||||
String distanceDisplay = "";
|
String distanceDisplay = "";
|
||||||
@@ -151,7 +157,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_buildMiniStatChip(
|
_buildMiniStatChip(
|
||||||
icon: Icons.access_time_filled_rounded,
|
icon: Icons.access_time_filled_rounded,
|
||||||
text: "$minutes ${'min'.tr}",
|
text: minutes > 0 ? "$minutes ${'min'.tr}" : "--",
|
||||||
color: AppColor.primaryColor,
|
color: AppColor.primaryColor,
|
||||||
bgColor: AppColor.primaryColor.withOpacity(0.1),
|
bgColor: AppColor.primaryColor.withOpacity(0.1),
|
||||||
),
|
),
|
||||||
@@ -229,7 +235,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
|||||||
// [MODIFIED] 2. كرت المعلومات المضغوط جداً
|
// [MODIFIED] 2. كرت المعلومات المضغوط جداً
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
Widget _buildCompactInfoCard(BuildContext context,
|
Widget _buildCompactInfoCard(BuildContext context,
|
||||||
MapPassengerController controller, Color Function(String) parseColor) {
|
RideLifecycleController controller, Color Function(String) parseColor) {
|
||||||
return Container(
|
return Container(
|
||||||
// تقليل الحواف الداخلية للكرت
|
// تقليل الحواف الداخلية للكرت
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
@@ -312,7 +318,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMicroCarIcon(
|
Widget _buildMicroCarIcon(
|
||||||
MapPassengerController controller, Color Function(String) parseColor) {
|
RideLifecycleController controller, Color Function(String) parseColor) {
|
||||||
Color carColor = parseColor(controller.colorHex);
|
Color carColor = parseColor(controller.colorHex);
|
||||||
return Container(
|
return Container(
|
||||||
height: 40, // تصغير من 50
|
height: 40, // تصغير من 50
|
||||||
@@ -343,9 +349,8 @@ class ApplyOrderWidget extends StatelessWidget {
|
|||||||
color: Get.isDarkMode ? Colors.grey[850] : const Color(0xFFF5F5F5),
|
color: Get.isDarkMode ? Colors.grey[850] : const Color(0xFFF5F5F5),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Get.isDarkMode
|
color:
|
||||||
? Colors.white10
|
Get.isDarkMode ? Colors.white10 : Colors.grey.withOpacity(0.3)),
|
||||||
: Colors.grey.withOpacity(0.3)),
|
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@@ -374,7 +379,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
|||||||
// [MODIFIED] 3. أزرار الاتصال (Slim Buttons)
|
// [MODIFIED] 3. أزرار الاتصال (Slim Buttons)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
Widget _buildCompactButtonsRow(
|
Widget _buildCompactButtonsRow(
|
||||||
BuildContext context, MapPassengerController controller) {
|
BuildContext context, RideLifecycleController controller) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 40, // تحديد ارتفاع ثابت وصغير للأزرار
|
height: 40, // تحديد ارتفاع ثابت وصغير للأزرار
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -397,7 +402,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
|||||||
bgColor: AppColor.greenColor,
|
bgColor: AppColor.greenColor,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
makePhoneCall(controller.driverPhone);
|
_showCallSelectionDialog(context, controller);
|
||||||
},
|
},
|
||||||
isPrimary: true,
|
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({
|
Widget _buildSlimButton({
|
||||||
required String label,
|
required String label,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
@@ -444,7 +516,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
|||||||
|
|
||||||
// --- النوافذ المنبثقة للرسائل (نفس الكود السابق مع تحسين بسيط) ---
|
// --- النوافذ المنبثقة للرسائل (نفس الكود السابق مع تحسين بسيط) ---
|
||||||
void _showContactOptionsDialog(
|
void _showContactOptionsDialog(
|
||||||
BuildContext context, MapPassengerController controller) {
|
BuildContext context, RideLifecycleController controller) {
|
||||||
Get.bottomSheet(
|
Get.bottomSheet(
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
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 = [
|
const messages = [
|
||||||
'Hello, I\'m at the agreed-upon location',
|
'Hello, I\'m at the agreed-upon location',
|
||||||
'I\'m waiting for you',
|
'I\'m waiting for you',
|
||||||
@@ -510,7 +582,7 @@ class ApplyOrderWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCustomMessageInput(
|
Widget _buildCustomMessageInput(
|
||||||
MapPassengerController controller, BuildContext context) {
|
RideLifecycleController controller, BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
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(
|
NotificationService.sendNotification(
|
||||||
category: 'MSG_FROM_PASSENGER',
|
category: 'MSG_FROM_PASSENGER',
|
||||||
target: controller.driverToken.toString(),
|
target: controller.driverToken.toString(),
|
||||||
@@ -577,7 +664,7 @@ class DriverArrivePassengerAndWaitMinute extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@@ -619,7 +706,7 @@ class TimeDriverToPassenger extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||||
if (controller.timeToPassengerFromDriverAfterApplied <= 0) {
|
if (controller.timeToPassengerFromDriverAfterApplied <= 0) {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import 'package:get/get.dart';
|
|||||||
import 'package:Intaleq/controller/payment/payment_controller.dart';
|
import 'package:Intaleq/controller/payment/payment_controller.dart';
|
||||||
|
|
||||||
import '../../../constant/style.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());
|
Get.put(PaymentController());
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<RideLifecycleController>(
|
||||||
builder: (controller) =>
|
builder: (controller) =>
|
||||||
controller.isBottomSheetShown && controller.rideConfirm == false
|
controller.isBottomSheetShown && controller.rideConfirm == false
|
||||||
? const Positioned(
|
? const Positioned(
|
||||||
@@ -508,7 +508,7 @@ class Details extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<RideLifecycleController>(
|
||||||
builder: (controller) => Column(
|
builder: (controller) => Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:Intaleq/constant/colors.dart';
|
import 'package:Intaleq/constant/colors.dart';
|
||||||
import 'package:Intaleq/constant/style.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';
|
import '../../widgets/elevated_btn.dart';
|
||||||
|
|
||||||
// دالة لإظهار الشيت
|
// دالة لإظهار الشيت
|
||||||
@@ -21,7 +21,7 @@ class CancelRidePageWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// تأكد من وجود الكنترولر
|
// تأكد من وجود الكنترولر
|
||||||
final controller = Get.find<MapPassengerController>();
|
final controller = Get.find<RideLifecycleController>();
|
||||||
|
|
||||||
final List<String> reasons = [
|
final List<String> reasons = [
|
||||||
"Changed my mind".tr,
|
"Changed my mind".tr,
|
||||||
@@ -39,7 +39,7 @@ class CancelRidePageWidget extends StatelessWidget {
|
|||||||
color: AppColor.secondaryColor,
|
color: AppColor.secondaryColor,
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(25)),
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(25)),
|
||||||
),
|
),
|
||||||
child: GetBuilder<MapPassengerController>(
|
child: GetBuilder<RideLifecycleController>(
|
||||||
builder: (controller) => Column(
|
builder: (controller) => Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import 'dart:ui';
|
|||||||
|
|
||||||
import '../../../constant/info.dart';
|
import '../../../constant/info.dart';
|
||||||
import '../../../controller/functions/tts.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';
|
import '../../widgets/mydialoug.dart';
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -62,7 +62,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
CarDetailsTypeToChoose({super.key});
|
CarDetailsTypeToChoose({super.key});
|
||||||
final textToSpeechController = Get.find<TextToSpeechController>();
|
final textToSpeechController = Get.find<TextToSpeechController>();
|
||||||
|
|
||||||
void _prepareCarTypes(MapPassengerController controller) {
|
void _prepareCarTypes(RideLifecycleController controller) {
|
||||||
if (controller.distance > 23) {
|
if (controller.distance > 23) {
|
||||||
if (!carTypes.any((car) => car.carType == 'Rayeh Gai')) {
|
if (!carTypes.any((car) => car.carType == 'Rayeh Gai')) {
|
||||||
carTypes.add(CarType(
|
carTypes.add(CarType(
|
||||||
@@ -77,7 +77,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||||
_prepareCarTypes(controller);
|
_prepareCarTypes(controller);
|
||||||
|
|
||||||
if (!(controller.isBottomSheetShown) && controller.rideConfirm == false) {
|
if (!(controller.isBottomSheetShown) && controller.rideConfirm == false) {
|
||||||
@@ -170,7 +170,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// HEADER
|
// HEADER
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
Widget _buildHeader(MapPassengerController controller) {
|
Widget _buildHeader(RideLifecycleController controller) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(22, 4, 22, 8),
|
padding: const EdgeInsets.fromLTRB(22, 4, 22, 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -290,7 +290,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// CAR CARD
|
// CAR CARD
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
Widget _buildCarCard(BuildContext context, MapPassengerController controller,
|
Widget _buildCarCard(BuildContext context, RideLifecycleController controller,
|
||||||
CarType carType, bool isSelected, int index) {
|
CarType carType, bool isSelected, int index) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -437,7 +437,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
// PROMO BUTTON
|
// PROMO BUTTON
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
Widget _buildPromoButton(
|
Widget _buildPromoButton(
|
||||||
BuildContext context, MapPassengerController controller) {
|
BuildContext context, RideLifecycleController controller) {
|
||||||
if (controller.promoTaken) return const SizedBox.shrink();
|
if (controller.promoTaken) return const SizedBox.shrink();
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -511,7 +511,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// NEGATIVE BALANCE WARNING
|
// NEGATIVE BALANCE WARNING
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
Widget _buildNegativeBalanceWarning(MapPassengerController controller) {
|
Widget _buildNegativeBalanceWarning(RideLifecycleController controller) {
|
||||||
final passengerWallet =
|
final passengerWallet =
|
||||||
double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ?? 0.0;
|
double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ?? 0.0;
|
||||||
if (passengerWallet < 0.0) {
|
if (passengerWallet < 0.0) {
|
||||||
@@ -556,7 +556,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
// PRICING HELPERS (Unchanged logic)
|
// PRICING HELPERS (Unchanged logic)
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
String _getPassengerPriceText(
|
String _getPassengerPriceText(
|
||||||
CarType carType, MapPassengerController mapPassengerController) {
|
CarType carType, RideLifecycleController mapPassengerController) {
|
||||||
double rawPrice;
|
double rawPrice;
|
||||||
switch (carType.carType) {
|
switch (carType.carType) {
|
||||||
case 'Comfort':
|
case 'Comfort':
|
||||||
@@ -596,7 +596,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
// DIALOGS
|
// DIALOGS
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
void _showPromoCodeDialog(
|
void _showPromoCodeDialog(
|
||||||
BuildContext context, MapPassengerController controller) {
|
BuildContext context, RideLifecycleController controller) {
|
||||||
Get.dialog(
|
Get.dialog(
|
||||||
Dialog(
|
Dialog(
|
||||||
shape:
|
shape:
|
||||||
@@ -671,7 +671,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
|
|
||||||
void _showCarDetailsDialog(
|
void _showCarDetailsDialog(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MapPassengerController mapPassengerController,
|
RideLifecycleController mapPassengerController,
|
||||||
CarType carType,
|
CarType carType,
|
||||||
TextToSpeechController textToSpeechController) {
|
TextToSpeechController textToSpeechController) {
|
||||||
Get.dialog(
|
Get.dialog(
|
||||||
@@ -843,7 +843,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
// LOGIC HELPERS (Unchanged)
|
// LOGIC HELPERS (Unchanged)
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
String _getCarDescription(
|
String _getCarDescription(
|
||||||
MapPassengerController mapPassengerController, CarType carType) {
|
RideLifecycleController mapPassengerController, CarType carType) {
|
||||||
switch (carType.carType) {
|
switch (carType.carType) {
|
||||||
case 'Comfort':
|
case 'Comfort':
|
||||||
return mapPassengerController.endNameAddress
|
return mapPassengerController.endNameAddress
|
||||||
@@ -881,7 +881,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleCarSelection(BuildContext context,
|
void _handleCarSelection(BuildContext context,
|
||||||
MapPassengerController mapPassengerController, CarType carType) {
|
RideLifecycleController mapPassengerController, CarType carType) {
|
||||||
box.write(BoxName.carType, carType.carType);
|
box.write(BoxName.carType, carType.carType);
|
||||||
mapPassengerController.totalPassenger =
|
mapPassengerController.totalPassenger =
|
||||||
_getOriginalPrice(carType, mapPassengerController);
|
_getOriginalPrice(carType, mapPassengerController);
|
||||||
@@ -932,7 +932,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
double _getOriginalPrice(
|
double _getOriginalPrice(
|
||||||
CarType carType, MapPassengerController mapPassengerController) {
|
CarType carType, RideLifecycleController mapPassengerController) {
|
||||||
switch (carType.carType) {
|
switch (carType.carType) {
|
||||||
case 'Comfort':
|
case 'Comfort':
|
||||||
return mapPassengerController.totalPassengerComfort;
|
return mapPassengerController.totalPassengerComfort;
|
||||||
@@ -953,7 +953,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _buildRayehGaiOption(
|
Widget _buildRayehGaiOption(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MapPassengerController mapPassengerController,
|
RideLifecycleController mapPassengerController,
|
||||||
String carTypeName,
|
String carTypeName,
|
||||||
double price) {
|
double price) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@@ -983,7 +983,7 @@ class BurcMoney extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<RideLifecycleController>(
|
||||||
builder: (mapPassengerController) {
|
builder: (mapPassengerController) {
|
||||||
final passengerWallet =
|
final passengerWallet =
|
||||||
double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ??
|
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/colors.dart';
|
||||||
import '../../../constant/info.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 '../../../controller/payment/payment_controller.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../widgets/elevated_btn.dart';
|
import '../../widgets/elevated_btn.dart';
|
||||||
@@ -17,7 +17,7 @@ class CashConfirmPageShown extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||||
// شرط الإظهار الرئيسي لم يتغير
|
// شرط الإظهار الرئيسي لم يتغير
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../../constant/style.dart';
|
import '../../../constant/style.dart';
|
||||||
import '../../../controller/home/map_passenger_controller.dart';
|
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||||
import 'hexegone_clipper.dart';
|
import 'hexegone_clipper.dart';
|
||||||
|
|
||||||
GetBuilder<MapPassengerController> hexagonClipper() {
|
GetBuilder<RideLifecycleController> hexagonClipper() {
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<RideLifecycleController>(
|
||||||
builder: ((controller) => controller.rideConfirm
|
builder: ((controller) => controller.rideConfirm
|
||||||
? Positioned(
|
? Positioned(
|
||||||
top: Get.height * .1,
|
top: Get.height * .1,
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import 'package:intl/intl.dart';
|
|||||||
// import 'package:intl/intl.dart';
|
// import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import '../../../constant/style.dart';
|
import '../../../constant/style.dart';
|
||||||
import '../../../controller/home/map_passenger_controller.dart';
|
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||||
|
|
||||||
class DriverTimeArrivePassengerPage extends StatelessWidget {
|
class DriverTimeArrivePassengerPage extends StatelessWidget {
|
||||||
const DriverTimeArrivePassengerPage({super.key});
|
const DriverTimeArrivePassengerPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<RideLifecycleController>(
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
return controller.remainingTime == 0
|
return controller.remainingTime == 0
|
||||||
? Positioned(
|
? Positioned(
|
||||||
|
|||||||
@@ -12,54 +12,51 @@ import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
|||||||
import '../../../constant/colors.dart';
|
import '../../../constant/colors.dart';
|
||||||
import '../../../constant/style.dart';
|
import '../../../constant/style.dart';
|
||||||
import '../../../controller/functions/toast.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';
|
import '../../../main.dart';
|
||||||
|
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
// -- Widget for Destination Point Search (Optimized) --
|
// -- Widget for Destination Point Search (Optimized) --
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
|
|
||||||
/// A more optimized and cleaner implementation of the destination search form.
|
GetBuilder<LocationSearchController> formSearchPlacesDestenation() {
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
final String addWorkValue =
|
final String addWorkValue =
|
||||||
box.read(BoxName.addWork)?.toString() ?? 'addWork';
|
box.read(BoxName.addWork)?.toString() ?? 'addWork';
|
||||||
final String addHomeValue =
|
final String addHomeValue =
|
||||||
box.read(BoxName.addHome)?.toString() ?? 'addHome';
|
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) {
|
if (addWorkValue.isEmpty || addHomeValue.isEmpty) {
|
||||||
box.write(BoxName.addWork, 'addWork');
|
box.write(BoxName.addWork, 'addWork');
|
||||||
box.write(BoxName.addHome, 'addHome');
|
box.write(BoxName.addHome, 'addHome');
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<LocationSearchController>(
|
||||||
id: 'destination_form', // Use an ID to allow targeted updates
|
id: 'destination_form',
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
|
final mapEngine = Get.find<MapEngineController>();
|
||||||
|
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// --- Widget for the search text field ---
|
_SearchField(
|
||||||
_SearchField(controller: controller),
|
controller: controller,
|
||||||
|
mapEngine: mapEngine,
|
||||||
// --- Widget for "Add Work" and "Add Home" buttons ---
|
rideLifecycle: rideLifecycle,
|
||||||
|
),
|
||||||
_QuickActions(
|
_QuickActions(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
mapEngine: mapEngine,
|
||||||
|
rideLifecycle: rideLifecycle,
|
||||||
addWorkValue: addWorkValue,
|
addWorkValue: addWorkValue,
|
||||||
addHomeValue: addHomeValue,
|
addHomeValue: addHomeValue,
|
||||||
),
|
),
|
||||||
|
_SearchResults(
|
||||||
// --- Widget for displaying search results, wrapped in its own GetBuilder ---
|
controller: controller,
|
||||||
_SearchResults(),
|
mapEngine: mapEngine,
|
||||||
|
rideLifecycle: rideLifecycle,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -70,11 +67,16 @@ GetBuilder<MapPassengerController> formSearchPlacesDestenation() {
|
|||||||
// -- Private Helper Widgets for Cleaner Code --
|
// -- Private Helper Widgets for Cleaner Code --
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
|
|
||||||
/// A dedicated widget for the search input field.
|
|
||||||
class _SearchField extends StatefulWidget {
|
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
|
@override
|
||||||
State<_SearchField> createState() => _SearchFieldState();
|
State<_SearchField> createState() => _SearchFieldState();
|
||||||
@@ -83,7 +85,6 @@ class _SearchField extends StatefulWidget {
|
|||||||
class _SearchFieldState extends State<_SearchField> {
|
class _SearchFieldState extends State<_SearchField> {
|
||||||
Timer? _debounce;
|
Timer? _debounce;
|
||||||
|
|
||||||
// --- [إصلاح] Listener لتحديث الواجهة عند تغيير النص لإظهار/إخفاء زر المسح ---
|
|
||||||
void _onTextChanged() {
|
void _onTextChanged() {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
@@ -93,20 +94,18 @@ class _SearchFieldState extends State<_SearchField> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Add listener to update the suffix icon when text changes
|
|
||||||
widget.controller.placeDestinationController.addListener(_onTextChanged);
|
widget.controller.placeDestinationController.addListener(_onTextChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- [تحسين] إضافة Debouncer لتأخير البحث أثناء الكتابة ---
|
|
||||||
void _onSearchChanged(String query) {
|
void _onSearchChanged(String query) {
|
||||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||||
_debounce = Timer(const Duration(milliseconds: 500), () {
|
_debounce = Timer(const Duration(milliseconds: 500), () {
|
||||||
if (query.length > 2) {
|
if (query.length > 2) {
|
||||||
widget.controller.getPlaces();
|
widget.controller.getPlaces();
|
||||||
widget.controller.changeHeightPlaces();
|
widget.mapEngine.changeHeightPlaces();
|
||||||
} else if (query.isEmpty) {
|
} else if (query.isEmpty) {
|
||||||
widget.controller.clearPlacesDestination();
|
widget.controller.clearPlacesDestination();
|
||||||
widget.controller.changeHeightPlaces();
|
widget.mapEngine.changeHeightPlaces();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -114,7 +113,6 @@ class _SearchFieldState extends State<_SearchField> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_debounce?.cancel();
|
_debounce?.cancel();
|
||||||
// Remove the listener to prevent memory leaks
|
|
||||||
widget.controller.placeDestinationController.removeListener(_onTextChanged);
|
widget.controller.placeDestinationController.removeListener(_onTextChanged);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -133,18 +131,15 @@ class _SearchFieldState extends State<_SearchField> {
|
|||||||
hintText: widget.controller.hintTextDestinationPoint,
|
hintText: widget.controller.hintTextDestinationPoint,
|
||||||
hintStyle: AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
hintStyle: AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
||||||
prefixIcon: Icon(Icons.search, color: AppColor.primaryColor),
|
prefixIcon: Icon(Icons.search, color: AppColor.primaryColor),
|
||||||
// --- [إصلاح] تم استبدال Obx بشرط بسيط لأن `setState` يعيد بناء الواجهة الآن ---
|
|
||||||
suffixIcon: widget
|
suffixIcon: widget
|
||||||
.controller.placeDestinationController.text.isNotEmpty
|
.controller.placeDestinationController.text.isNotEmpty
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
widget.controller.placeDestinationController.clear();
|
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(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16.0, vertical: 10.0),
|
horizontal: 16.0, vertical: 10.0),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
@@ -163,12 +158,12 @@ class _SearchFieldState extends State<_SearchField> {
|
|||||||
const SizedBox(width: 8.0),
|
const SizedBox(width: 8.0),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
widget.controller.changeMainBottomMenuMap();
|
widget.mapEngine.changeMainBottomMenuMap();
|
||||||
widget.controller.changePickerShown();
|
widget.mapEngine.changePickerShown();
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.location_on_outlined,
|
icon: Icon(Icons.location_on_outlined,
|
||||||
color: AppColor.accentColor, size: 30),
|
color: AppColor.accentColor, size: 30),
|
||||||
tooltip: widget.controller.isAnotherOreder
|
tooltip: widget.rideLifecycle.isAnotherOreder
|
||||||
? 'Pick destination on map'.tr
|
? 'Pick destination on map'.tr
|
||||||
: 'Pick 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 {
|
class _QuickActions extends StatelessWidget {
|
||||||
final MapPassengerController controller;
|
final LocationSearchController controller;
|
||||||
|
final MapEngineController mapEngine;
|
||||||
|
final RideLifecycleController rideLifecycle;
|
||||||
final String addWorkValue;
|
final String addWorkValue;
|
||||||
final String addHomeValue;
|
final String addHomeValue;
|
||||||
|
|
||||||
const _QuickActions({
|
const _QuickActions({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
|
required this.mapEngine,
|
||||||
|
required this.rideLifecycle,
|
||||||
required this.addWorkValue,
|
required this.addWorkValue,
|
||||||
required this.addHomeValue,
|
required this.addHomeValue,
|
||||||
});
|
});
|
||||||
@@ -203,13 +201,20 @@ class _QuickActions extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
if (addWorkValue == 'addWork') {
|
if (addWorkValue == 'addWork') {
|
||||||
controller.workLocationFromMap = true;
|
controller.workLocationFromMap = true;
|
||||||
controller.changeMainBottomMenuMap();
|
mapEngine.changeMainBottomMenuMap();
|
||||||
controller.changePickerShown();
|
mapEngine.changePickerShown();
|
||||||
} else {
|
} 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(
|
_buildQuickActionButton(
|
||||||
icon: Icons.home_outlined,
|
icon: Icons.home_outlined,
|
||||||
@@ -217,13 +222,20 @@ class _QuickActions extends StatelessWidget {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
if (addHomeValue == 'addHome') {
|
if (addHomeValue == 'addHome') {
|
||||||
controller.homeLocationFromMap = true;
|
controller.homeLocationFromMap = true;
|
||||||
controller.changeMainBottomMenuMap();
|
mapEngine.changeMainBottomMenuMap();
|
||||||
controller.changePickerShown();
|
mapEngine.changePickerShown();
|
||||||
} else {
|
} 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 {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<LocationSearchController>(
|
||||||
id: 'places_list', // Use a specific ID for targeted updates
|
id: 'places_list',
|
||||||
builder: (controller) {
|
builder: (locCtrl) {
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
height: controller.placesDestination.isNotEmpty ? 300 : 0,
|
height: locCtrl.placesDestination.isNotEmpty ? 300 : 0,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
@@ -250,11 +270,11 @@ class _SearchResults extends StatelessWidget {
|
|||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const ClampingScrollPhysics(),
|
physics: const ClampingScrollPhysics(),
|
||||||
itemCount: controller.placesDestination.length,
|
itemCount: locCtrl.placesDestination.length,
|
||||||
separatorBuilder: (context, index) =>
|
separatorBuilder: (context, index) =>
|
||||||
const Divider(height: 1, color: Colors.grey),
|
const Divider(height: 1, color: Colors.grey),
|
||||||
itemBuilder: (BuildContext context, int index) {
|
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 title = res['name_ar'] ?? res['name'] ?? 'Unknown Place';
|
||||||
final address = res['address'] ?? 'Details not available';
|
final address = res['address'] ?? 'Details not available';
|
||||||
final latitude = res['latitude'];
|
final latitude = res['latitude'];
|
||||||
@@ -277,7 +297,14 @@ class _SearchResults extends StatelessWidget {
|
|||||||
context, latitude, longitude, title),
|
context, latitude, longitude, title),
|
||||||
),
|
),
|
||||||
onTap: () => _handlePlaceSelection(
|
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,
|
Future<void> _handleAddToFavorites(BuildContext context, dynamic latitude,
|
||||||
dynamic longitude, String title) async {
|
dynamic longitude, String title) async {
|
||||||
if (latitude != null && longitude != null) {
|
if (latitude != null && longitude != null) {
|
||||||
@@ -311,14 +337,19 @@ class _SearchResults extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handlePlaceSelection(MapPassengerController controller,
|
Future<void> _handlePlaceSelection(
|
||||||
dynamic latitude, dynamic longitude, String title, int index) async {
|
LocationSearchController controller,
|
||||||
|
MapEngineController mapEngine,
|
||||||
|
RideLifecycleController rideLifecycle,
|
||||||
|
dynamic latitude,
|
||||||
|
dynamic longitude,
|
||||||
|
String title,
|
||||||
|
int index) async {
|
||||||
if (latitude == null || longitude == null) {
|
if (latitude == null || longitude == null) {
|
||||||
Toast.show(Get.context!, 'Invalid location data', AppColor.redColor);
|
Toast.show(Get.context!, 'Invalid location data', AppColor.redColor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to recent locations
|
|
||||||
await sql.insertMapLocation({
|
await sql.insertMapLocation({
|
||||||
'latitude': latitude,
|
'latitude': latitude,
|
||||||
'longitude': longitude,
|
'longitude': longitude,
|
||||||
@@ -330,53 +361,55 @@ class _SearchResults extends StatelessWidget {
|
|||||||
final destLatLng = LatLng(
|
final destLatLng = LatLng(
|
||||||
double.parse(latitude.toString()), double.parse(longitude.toString()));
|
double.parse(latitude.toString()), double.parse(longitude.toString()));
|
||||||
|
|
||||||
if (controller.isAnotherOreder) {
|
if (rideLifecycle.isAnotherOreder) {
|
||||||
// **Another Order Flow**
|
await _handleAnotherOrderSelection(
|
||||||
await _handleAnotherOrderSelection(controller, destLatLng);
|
controller, mapEngine, rideLifecycle, destLatLng);
|
||||||
} else {
|
} else {
|
||||||
// **Regular Order Flow**
|
_handleRegularOrderSelection(
|
||||||
_handleRegularOrderSelection(controller, destLatLng, index);
|
controller, mapEngine, rideLifecycle, destLatLng, index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleAnotherOrderSelection(
|
Future<void> _handleAnotherOrderSelection(
|
||||||
MapPassengerController controller, LatLng destination) async {
|
LocationSearchController controller,
|
||||||
|
MapEngineController mapEngine,
|
||||||
|
RideLifecycleController rideLifecycle,
|
||||||
|
LatLng destination) async {
|
||||||
controller.myDestination = destination;
|
controller.myDestination = destination;
|
||||||
controller.clearPlacesDestination(); // Helper method in controller
|
controller.clearPlacesDestination();
|
||||||
|
|
||||||
await controller.getDirectionMap(
|
await rideLifecycle.getDirectionMap(
|
||||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
||||||
'${controller.myDestination.latitude},${controller.myDestination.longitude}');
|
'${controller.myDestination.latitude},${controller.myDestination.longitude}');
|
||||||
|
|
||||||
controller.isPickerShown = false;
|
mapEngine.isPickerShown = false;
|
||||||
controller.passengerStartLocationFromMap = false;
|
controller.passengerStartLocationFromMap = false;
|
||||||
controller.changeMainBottomMenuMap();
|
mapEngine.changeMainBottomMenuMap();
|
||||||
controller.showBottomSheet1();
|
rideLifecycle.showBottomSheet1();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleRegularOrderSelection(
|
void _handleRegularOrderSelection(
|
||||||
MapPassengerController controller, LatLng destination, int index) {
|
LocationSearchController controller,
|
||||||
|
MapEngineController mapEngine,
|
||||||
|
RideLifecycleController rideLifecycle,
|
||||||
|
LatLng destination,
|
||||||
|
int index) {
|
||||||
controller.passengerLocation = controller.newMyLocation;
|
controller.passengerLocation = controller.newMyLocation;
|
||||||
controller.myDestination = destination;
|
controller.myDestination = destination;
|
||||||
controller.convertHintTextDestinationNewPlaces(index);
|
controller.convertHintTextDestinationNewPlaces(index);
|
||||||
|
|
||||||
controller.clearPlacesDestination(); // Helper method in controller
|
controller.clearPlacesDestination();
|
||||||
|
|
||||||
controller.changeMainBottomMenuMap();
|
mapEngine.changeMainBottomMenuMap();
|
||||||
controller.passengerStartLocationFromMap = true;
|
controller.passengerStartLocationFromMap = true;
|
||||||
controller.isPickerShown = true;
|
mapEngine.isPickerShown = true;
|
||||||
|
|
||||||
// ✅ FIX: Draw the route after setting destination (matching the "Another Order" flow)
|
rideLifecycle.getDirectionMap(
|
||||||
controller.getDirectionMap(
|
|
||||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
||||||
'${controller.myDestination.latitude},${controller.myDestination.longitude}');
|
'${controller.myDestination.latitude},${controller.myDestination.longitude}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------
|
|
||||||
// -- Helper Functions (kept from original code) --
|
|
||||||
// ---------------------------------------------------
|
|
||||||
|
|
||||||
Widget _buildQuickActionButton({
|
Widget _buildQuickActionButton({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String text,
|
required String text,
|
||||||
@@ -410,10 +443,12 @@ Widget _buildQuickActionButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showChangeLocationDialog(
|
void _showChangeLocationDialog(LocationSearchController controller,
|
||||||
MapPassengerController controller, String locationType) {
|
MapEngineController mapEngine, String locationType) {
|
||||||
MyDialog().getDialog(
|
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') {
|
if (locationType == 'Work') {
|
||||||
@@ -421,15 +456,18 @@ void _showChangeLocationDialog(
|
|||||||
} else {
|
} else {
|
||||||
controller.homeLocationFromMap = true;
|
controller.homeLocationFromMap = true;
|
||||||
}
|
}
|
||||||
controller.changeMainBottomMenuMap();
|
mapEngine.changeMainBottomMenuMap();
|
||||||
controller.changePickerShown();
|
mapEngine.changePickerShown();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleQuickAction(
|
void _handleQuickAction(
|
||||||
MapPassengerController controller, String boxName, String hintText) async {
|
LocationSearchController controller,
|
||||||
// --- [تحسين] قراءة وتحويل الإحداثيات بأمان أكبر ---
|
MapEngineController mapEngine,
|
||||||
|
RideLifecycleController rideLifecycle,
|
||||||
|
String boxName,
|
||||||
|
String hintText) async {
|
||||||
try {
|
try {
|
||||||
final locationString = box.read(boxName).toString();
|
final locationString = box.read(boxName).toString();
|
||||||
final parts = locationString.split(',');
|
final parts = locationString.split(',');
|
||||||
@@ -439,20 +477,19 @@ void _handleQuickAction(
|
|||||||
);
|
);
|
||||||
|
|
||||||
controller.hintTextDestinationPoint = hintText;
|
controller.hintTextDestinationPoint = hintText;
|
||||||
controller.changeMainBottomMenuMap();
|
mapEngine.changeMainBottomMenuMap();
|
||||||
|
|
||||||
await controller.getDirectionMap(
|
await rideLifecycle.getDirectionMap(
|
||||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
||||||
'${latLng.latitude},${latLng.longitude}',
|
'${latLng.latitude},${latLng.longitude}',
|
||||||
);
|
);
|
||||||
|
|
||||||
controller.currentLocationToFormPlaces = false;
|
controller.currentLocationToFormPlaces = false;
|
||||||
controller.clearPlacesDestination(); // Helper method in controller
|
controller.clearPlacesDestination();
|
||||||
controller.passengerStartLocationFromMap = false;
|
controller.passengerStartLocationFromMap = false;
|
||||||
controller.isPickerShown = false;
|
mapEngine.isPickerShown = false;
|
||||||
controller.showBottomSheet1();
|
rideLifecycle.showBottomSheet1();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Handle error if parsing fails
|
|
||||||
Log.print("Error handling quick action: $e");
|
Log.print("Error handling quick action: $e");
|
||||||
Toast.show(Get.context!, "Failed to get location".tr, AppColor.redColor);
|
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/colors.dart';
|
||||||
import '../../../constant/style.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) --
|
// -- Widget for Start Point Search (Updated) --
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
|
|
||||||
GetBuilder<MapPassengerController> formSearchPlacesStart() {
|
GetBuilder<LocationSearchController> formSearchPlacesStart() {
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<LocationSearchController>(
|
||||||
id: 'start_point_form', // إضافة معرف لتحديث هذا الجزء فقط عند الحاجة
|
id: 'start_point_form',
|
||||||
builder: (controller) => Column(
|
builder: (controller) {
|
||||||
children: [
|
final mapEngine = Get.find<MapEngineController>();
|
||||||
Padding(
|
return Column(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
children: [
|
||||||
child: Row(
|
Padding(
|
||||||
children: [
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
// --- حقل البحث النصي ---
|
child: Row(
|
||||||
Expanded(
|
children: [
|
||||||
child: TextFormField(
|
Expanded(
|
||||||
controller: controller.placeStartController,
|
child: TextFormField(
|
||||||
onChanged: (value) {
|
controller: controller.placeStartController,
|
||||||
if (controller.placeStartController.text.length > 2) {
|
onChanged: (value) {
|
||||||
controller.getPlacesStart();
|
if (controller.placeStartController.text.length > 2) {
|
||||||
} else if (controller.placeStartController.text.isEmpty) {
|
controller.getPlacesStart();
|
||||||
controller.clearPlacesStart();
|
} else if (controller.placeStartController.text.isEmpty) {
|
||||||
}
|
controller.clearPlacesStart();
|
||||||
},
|
}
|
||||||
decoration: InputDecoration(
|
},
|
||||||
hintText: 'Search for a starting point'.tr,
|
decoration: InputDecoration(
|
||||||
hintStyle:
|
hintText: 'Search for a starting point'.tr,
|
||||||
AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
hintStyle:
|
||||||
prefixIcon:
|
AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
||||||
Icon(Icons.search, color: AppColor.primaryColor),
|
prefixIcon:
|
||||||
suffixIcon: controller.placeStartController.text.isNotEmpty
|
Icon(Icons.search, color: AppColor.primaryColor),
|
||||||
? IconButton(
|
suffixIcon: controller.placeStartController.text.isNotEmpty
|
||||||
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
? IconButton(
|
||||||
onPressed: () {
|
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
||||||
controller.placeStartController.clear();
|
onPressed: () {
|
||||||
controller.clearPlacesStart();
|
controller.placeStartController.clear();
|
||||||
},
|
controller.clearPlacesStart();
|
||||||
)
|
},
|
||||||
: null,
|
)
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
: null,
|
||||||
horizontal: 16.0, vertical: 10.0),
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
border: OutlineInputBorder(
|
horizontal: 16.0, vertical: 10.0),
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
border: OutlineInputBorder(
|
||||||
borderSide: BorderSide.none,
|
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(
|
||||||
const SizedBox(width: 8.0),
|
onPressed: () {
|
||||||
|
controller.passengerStartLocationFromMap = true;
|
||||||
// --- أيقونة اختيار الموقع من الخريطة (الجزء المضاف) ---
|
mapEngine.changeMainBottomMenuMap();
|
||||||
IconButton(
|
mapEngine.changePickerShown();
|
||||||
onPressed: () {
|
},
|
||||||
// هذا السطر مهم جداً: نخبر الكونترولر أننا نحدد نقطة البداية الآن
|
icon: Icon(Icons.location_on_outlined,
|
||||||
controller.passengerStartLocationFromMap = true;
|
color: AppColor.accentColor, size: 30),
|
||||||
|
tooltip: 'Pick start point on map'.tr,
|
||||||
// إخفاء القائمة السفلية وفتح مؤشر الخريطة (Picker)
|
),
|
||||||
controller.changeMainBottomMenuMap();
|
],
|
||||||
controller.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';
|
||||||
|
|
||||||
// --- قائمة نتائج البحث ---
|
return ListTile(
|
||||||
AnimatedContainer(
|
leading: const Icon(Icons.place, size: 30, color: Colors.grey),
|
||||||
duration: const Duration(milliseconds: 200),
|
title: Text(title,
|
||||||
height: controller.placesStart.isNotEmpty ? 300 : 0,
|
style: AppStyle.subtitle
|
||||||
decoration: BoxDecoration(
|
.copyWith(fontWeight: FontWeight.w500)),
|
||||||
color: Colors.white,
|
subtitle: Text(address,
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
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/colors.dart';
|
||||||
import '../../../constant/style.dart';
|
import '../../../constant/style.dart';
|
||||||
import '../../../controller/functions/toast.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';
|
import '../../../main.dart';
|
||||||
|
|
||||||
GetBuilder<MapPassengerController> formSearchPlaces(int index) {
|
GetBuilder<LocationSearchController> formSearchPlaces(int index) {
|
||||||
// DbSql sql = DbSql.instance;
|
return GetBuilder<LocationSearchController>(
|
||||||
return GetBuilder<MapPassengerController>(
|
builder: (controller) {
|
||||||
builder: (controller) => Column(
|
final mapEngine = Get.find<MapEngineController>();
|
||||||
children: [
|
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||||
Padding(
|
return Column(
|
||||||
padding: const EdgeInsets.all(16),
|
children: [
|
||||||
child: Container(
|
Padding(
|
||||||
decoration: BoxDecoration(color: AppColor.secondaryColor),
|
padding: const EdgeInsets.all(16),
|
||||||
child: TextField(
|
child: Container(
|
||||||
decoration: InputDecoration(
|
decoration: BoxDecoration(color: AppColor.secondaryColor),
|
||||||
border: const OutlineInputBorder(
|
child: TextField(
|
||||||
borderRadius: BorderRadius.only(),
|
decoration: InputDecoration(
|
||||||
gapPadding: 4,
|
border: const OutlineInputBorder(
|
||||||
borderSide: BorderSide(
|
borderRadius: BorderRadius.only(),
|
||||||
color: AppColor.redColor,
|
gapPadding: 4,
|
||||||
width: 2,
|
borderSide: BorderSide(
|
||||||
)),
|
color: AppColor.redColor,
|
||||||
suffixIcon: const Icon(Icons.search),
|
width: 2,
|
||||||
hintText: controller.hintTextwayPoint0.tr,
|
)),
|
||||||
hintStyle: AppStyle.title,
|
suffixIcon: const Icon(Icons.search),
|
||||||
hintMaxLines: 1,
|
hintText: controller.hintTextwayPoint0.tr,
|
||||||
prefixIcon: IconButton(
|
hintStyle: AppStyle.title,
|
||||||
onPressed: () {
|
hintMaxLines: 1,
|
||||||
controller.allTextEditingPlaces[index].clear();
|
prefixIcon: IconButton(
|
||||||
controller.clearPlaces(index);
|
onPressed: () {
|
||||||
},
|
controller.allTextEditingPlaces[index].clear();
|
||||||
icon: Icon(
|
controller.clearPlaces(index);
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
// 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.placeListResponseAll[index].isEmpty
|
||||||
controller.startLocationFromMapAll[index] = true;
|
? InkWell(
|
||||||
controller.wayPointIndex = index;
|
onTap: () {
|
||||||
Get.back();
|
controller.startLocationFromMapAll[index] = true;
|
||||||
// controller.changeMainBottomMenuMap();
|
controller.wayPointIndex = index;
|
||||||
controller.changeWayPointStopsSheet();
|
Get.back();
|
||||||
controller.changePickerShown();
|
mapEngine.changeWayPointStopsSheet();
|
||||||
},
|
mapEngine.changePickerShown();
|
||||||
child: Text(
|
},
|
||||||
'Choose from Map'.tr + ' $index'.tr,
|
child: Text(
|
||||||
style:
|
'Choose from Map'.tr + ' $index'.tr,
|
||||||
AppStyle.title.copyWith(color: AppColor.blueColor),
|
style:
|
||||||
),
|
AppStyle.title.copyWith(color: AppColor.blueColor),
|
||||||
)
|
),
|
||||||
: const SizedBox(),
|
)
|
||||||
Container(
|
: const SizedBox(),
|
||||||
height: controller.placeListResponseAll[index].isNotEmpty
|
Container(
|
||||||
? controller.height
|
height: controller.placeListResponseAll[index].isNotEmpty
|
||||||
: 0,
|
? mapEngine.height
|
||||||
color: AppColor.secondaryColor,
|
: 0,
|
||||||
child: ListView.builder(
|
color: AppColor.secondaryColor,
|
||||||
itemCount: controller.placeListResponseAll[index].length,
|
child: ListView.builder(
|
||||||
itemBuilder: (BuildContext context, int i) {
|
itemCount: controller.placeListResponseAll[index].length,
|
||||||
var res = controller.placeListResponseAll[index][i];
|
itemBuilder: (BuildContext context, int i) {
|
||||||
return InkWell(
|
var res = controller.placeListResponseAll[index][i];
|
||||||
onTap: () async {
|
return InkWell(
|
||||||
// ── Extract selected location ──
|
onTap: () async {
|
||||||
final double lat = res['geometry']['location']['lat'];
|
final double lat = res['geometry']['location']['lat'];
|
||||||
final double lng = res['geometry']['location']['lng'];
|
final double lng = res['geometry']['location']['lng'];
|
||||||
final String placeName = res['name'].toString();
|
final String placeName = res['name'].toString();
|
||||||
final selectedLatLng = LatLng(lat, lng);
|
final selectedLatLng = LatLng(lat, lng);
|
||||||
|
|
||||||
controller.changeHeightPlaces();
|
mapEngine.changeHeightPlaces();
|
||||||
|
|
||||||
// ── Update start/end based on context ──
|
if (controller.currentLocationToFormPlacesAll[index] ==
|
||||||
if (controller.currentLocationToFormPlacesAll[index] ==
|
true) {
|
||||||
true) {
|
controller.newStartPointLocation =
|
||||||
controller.newStartPointLocation =
|
rideLifecycle.passengerLocation;
|
||||||
controller.passengerLocation;
|
} else {
|
||||||
} else {
|
rideLifecycle.passengerLocation =
|
||||||
controller.passengerLocation =
|
controller.newStartPointLocation;
|
||||||
controller.newStartPointLocation;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ FIX: Set the waypoint to the selected location
|
controller.menuWaypoints[index] = selectedLatLng;
|
||||||
controller.menuWaypoints[index] = selectedLatLng;
|
controller.menuWaypointNames[index] = placeName;
|
||||||
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 =
|
||||||
final String start =
|
'${rideLifecycle.passengerLocation.latitude},${rideLifecycle.passengerLocation.longitude}';
|
||||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}';
|
final String dest =
|
||||||
final String dest =
|
'${rideLifecycle.myDestination.latitude},${rideLifecycle.myDestination.longitude}';
|
||||||
'${controller.myDestination.latitude},${controller.myDestination.longitude}';
|
|
||||||
|
|
||||||
await controller.getDirectionMap(start, dest);
|
await rideLifecycle.getDirectionMap(start, dest);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Image.network(
|
||||||
children: [
|
res['icon'],
|
||||||
Image.network(
|
width: 20,
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
Column(
|
IconButton(
|
||||||
children: [
|
onPressed: () async {
|
||||||
Text(
|
await sql.insertMapLocation({
|
||||||
res['name'].toString(),
|
'latitude': res['geometry']
|
||||||
style: AppStyle.title,
|
['location']['lat'],
|
||||||
),
|
'longitude': res['geometry']
|
||||||
Text(
|
['location']['lng'],
|
||||||
res['vicinity'].toString(),
|
'name': res['name'].toString(),
|
||||||
style: AppStyle.subtitle,
|
'rate': res['rating'].toString(),
|
||||||
),
|
}, TableName.placesFavorite);
|
||||||
],
|
Toast.show(
|
||||||
),
|
context,
|
||||||
Column(
|
'${res['name']} ${'Saved Sucssefully'.tr}',
|
||||||
children: [
|
AppColor.primaryColor);
|
||||||
Text(
|
},
|
||||||
'rate',
|
icon: const Icon(Icons.favorite_border),
|
||||||
style: AppStyle.subtitle,
|
),
|
||||||
),
|
],
|
||||||
Text(
|
),
|
||||||
res['rating'].toString(),
|
Column(
|
||||||
style: AppStyle.subtitle,
|
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/controller/home/points_for_rider_controller.dart';
|
||||||
import 'package:Intaleq/services/offline_map_service.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/mycircular.dart';
|
||||||
import '../../widgets/mydialoug.dart';
|
import '../../widgets/mydialoug.dart';
|
||||||
|
|
||||||
class GoogleMapPassengerWidget extends StatelessWidget {
|
class GoogleMapPassengerWidget extends StatelessWidget {
|
||||||
GoogleMapPassengerWidget({super.key});
|
GoogleMapPassengerWidget({super.key});
|
||||||
|
|
||||||
final WayPointController wayPointController = Get.put(WayPointController());
|
final WayPointController wayPointController = Get.find<WayPointController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(
|
final locationSearch = Get.find<LocationSearchController>();
|
||||||
builder: (controller) => controller.isLoading
|
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||||
|
final nearbyDrivers = Get.find<NearbyDriversController>();
|
||||||
|
|
||||||
|
return GetBuilder<MapEngineController>(
|
||||||
|
builder: (controller) => rideLifecycle.isLoading
|
||||||
? const MyCircularProgressIndicator()
|
? const MyCircularProgressIndicator()
|
||||||
: Positioned(
|
: Positioned(
|
||||||
bottom: Get.height * .2,
|
bottom: Get.height * .2,
|
||||||
@@ -32,13 +39,13 @@ class GoogleMapPassengerWidget extends StatelessWidget {
|
|||||||
: 'assets/style.json',
|
: 'assets/style.json',
|
||||||
onMapCreated: controller.onMapCreated,
|
onMapCreated: controller.onMapCreated,
|
||||||
onStyleLoaded: controller.onStyleLoaded,
|
onStyleLoaded: controller.onStyleLoaded,
|
||||||
onCameraMove: controller.onCameraMoveThrottled,
|
onCameraMove: locationSearch.onCameraMoveThrottled,
|
||||||
onCameraIdle: () {
|
onCameraIdle: () {
|
||||||
if (controller.mapController != null) {
|
if (controller.mapController != null) {
|
||||||
final position = controller.mapController!.cameraPosition;
|
final position = controller.mapController!.cameraPosition;
|
||||||
if (position != null) {
|
if (position != null) {
|
||||||
Log.print('✅ onCameraIdle targeted: ${position.target}');
|
Log.print('✅ onCameraIdle targeted: ${position.target}');
|
||||||
controller
|
locationSearch
|
||||||
.updateCurrentLocationFromCamera(position.target);
|
.updateCurrentLocationFromCamera(position.target);
|
||||||
OfflineMapService.instance
|
OfflineMapService.instance
|
||||||
.downloadRegion(position.target, radiusKm: 1.0);
|
.downloadRegion(position.target, radiusKm: 1.0);
|
||||||
@@ -54,8 +61,8 @@ class GoogleMapPassengerWidget extends StatelessWidget {
|
|||||||
polygons: controller.polygons,
|
polygons: controller.polygons,
|
||||||
circles: controller.circles,
|
circles: controller.circles,
|
||||||
initialCameraPosition: CameraPosition(
|
initialCameraPosition: CameraPosition(
|
||||||
target: controller.passengerLocation,
|
target: locationSearch.passengerLocation,
|
||||||
zoom: controller.lowPerf ? 14.5 : 15,
|
zoom: nearbyDrivers.lowPerf ? 14.5 : 15,
|
||||||
),
|
),
|
||||||
myLocationEnabled: true,
|
myLocationEnabled: true,
|
||||||
onTap: (latlng) => controller.hidePlaces(),
|
onTap: (latlng) => controller.hidePlaces(),
|
||||||
@@ -63,11 +70,11 @@ class GoogleMapPassengerWidget extends StatelessWidget {
|
|||||||
MyDialog().getDialog('Are you want to go to this site'.tr, '',
|
MyDialog().getDialog('Are you want to go to this site'.tr, '',
|
||||||
() async {
|
() async {
|
||||||
controller.clearPolyline();
|
controller.clearPolyline();
|
||||||
controller.getDirectionMap(
|
rideLifecycle.getDirectionMap(
|
||||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
'${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}',
|
||||||
'${latlng.latitude},${latlng.longitude}',
|
'${latlng.latitude},${latlng.longitude}',
|
||||||
);
|
);
|
||||||
controller.showBottomSheet1();
|
rideLifecycle.showBottomSheet1();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,95 +1,82 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:Intaleq/views/widgets/elevated_btn.dart';
|
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:Intaleq/views/widgets/mycircular.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||||
import 'dart:ui'; // مهم لإضافة تأثير الضبابية
|
import 'dart:ui';
|
||||||
|
|
||||||
import '../../../constant/colors.dart';
|
import '../../../constant/colors.dart';
|
||||||
import '../../../controller/functions/tts.dart';
|
import '../../../controller/home/map/location_search_controller.dart';
|
||||||
import '../../../controller/home/map_passenger_controller.dart';
|
import '../../../controller/home/map/map_engine_controller.dart';
|
||||||
import '../../../controller/home/vip_waitting_page.dart';
|
import '../../../controller/home/vip_waitting_page.dart';
|
||||||
import '../navigation/navigation_view.dart';
|
import '../navigation/navigation_view.dart';
|
||||||
|
|
||||||
// --- الدالة الرئيسية بالتصميم الجديد ---
|
// --- الدالة الرئيسية بالتصميم الجديد ---
|
||||||
GetBuilder<MapPassengerController> leftMainMenuIcons() {
|
GetBuilder<MapEngineController> leftMainMenuIcons() {
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<MapEngineController>(
|
||||||
builder: (controller) => Positioned(
|
builder: (controller) {
|
||||||
// تم تعديل الموضع ليتناسب مع التصميم الجديد
|
final locationSearch = Get.find<LocationSearchController>();
|
||||||
top: Get.height * .01,
|
return Positioned(
|
||||||
left: 0,
|
top: Get.height * .01,
|
||||||
right: 0,
|
left: 0,
|
||||||
child: Center(
|
right: 0,
|
||||||
child: ClipRRect(
|
child: Center(
|
||||||
borderRadius: BorderRadius.circular(50.0), // لإنشاء شكل الكبسولة
|
child: ClipRRect(
|
||||||
child: BackdropFilter(
|
borderRadius: BorderRadius.circular(50.0),
|
||||||
filter: ImageFilter.blur(
|
child: BackdropFilter(
|
||||||
sigmaX: 8.0, sigmaY: 8.0), // تأثير الزجاج المصنفر
|
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.secondaryColor.withOpacity(0.4), // لون شبه شفاف
|
color: AppColor.secondaryColor.withValues(alpha: 0.4),
|
||||||
borderRadius: BorderRadius.circular(50.0),
|
borderRadius: BorderRadius.circular(50.0),
|
||||||
border: Border.all(color: AppColor.secondaryColor),
|
border: Border.all(color: AppColor.secondaryColor),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min, // ليأخذ الشريط حجم الأزرار فقط
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// --- تم استخدام دالة مساعدة جديدة للزر ---
|
_buildMapActionButton(
|
||||||
_buildMapActionButton(
|
icon: Icons.near_me_outlined,
|
||||||
icon: Icons.near_me_outlined,
|
tooltip: 'Toggle Map Type',
|
||||||
tooltip: 'Toggle Map Type',
|
onPressed: () => Get.to(() => NavigationView()),
|
||||||
onPressed: () => Get.to(() => NavigationView()),
|
),
|
||||||
),
|
_buildVerticalDivider(),
|
||||||
// _buildVerticalDivider(),
|
_buildMapActionButton(
|
||||||
// _buildMapActionButton(
|
icon: Icons.my_location_rounded,
|
||||||
// icon: Icons.traffic_outlined,
|
tooltip: 'Go to My Location',
|
||||||
// tooltip: 'Toggle Traffic',
|
onPressed: () {
|
||||||
// onPressed: () => controller.changeMapTraffic(),
|
controller.mapController?.animateCamera(
|
||||||
// ),
|
CameraUpdate.newLatLng(
|
||||||
_buildVerticalDivider(),
|
LatLng(
|
||||||
_buildMapActionButton(
|
locationSearch.passengerLocation.latitude,
|
||||||
icon: Icons.my_location_rounded,
|
locationSearch.passengerLocation.longitude,
|
||||||
tooltip: 'Go to My Location',
|
),
|
||||||
onPressed: () {
|
|
||||||
controller.mapController?.animateCamera(
|
|
||||||
CameraUpdate.newLatLng(
|
|
||||||
LatLng(
|
|
||||||
controller.passengerLocation.latitude,
|
|
||||||
controller.passengerLocation.longitude,
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
_buildVerticalDivider(),
|
||||||
_buildVerticalDivider(),
|
_buildMapActionButton(
|
||||||
_buildMapActionButton(
|
icon: Octicons.watch,
|
||||||
icon: Octicons.watch,
|
tooltip: 'VIP Waiting Page',
|
||||||
tooltip: 'VIP Waiting Page',
|
onPressed: () => Get.to(() => VipWaittingPage()),
|
||||||
onPressed: () => Get.to(() => VipWaittingPage()),
|
),
|
||||||
),
|
],
|
||||||
// _buildMapActionButton(
|
),
|
||||||
// icon: Octicons.ellipsis,
|
|
||||||
// tooltip: 'test',
|
|
||||||
// onPressed: () => Get.to(() => TestPage()),
|
|
||||||
// ),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- دالة مساعدة جديدة لإنشاء الأزرار بشكل أنيق ---
|
|
||||||
Widget _buildMapActionButton({
|
Widget _buildMapActionButton({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String tooltip,
|
required String tooltip,
|
||||||
@@ -101,28 +88,23 @@ Widget _buildMapActionButton({
|
|||||||
tooltip: tooltip,
|
tooltip: tooltip,
|
||||||
splashRadius: 22,
|
splashRadius: 22,
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
constraints: const BoxConstraints(), // لإزالة المساحات الافتراضية
|
constraints: const BoxConstraints(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ويدجت للفاصل الرأسي بين الأزرار ---
|
|
||||||
Widget _buildVerticalDivider() {
|
Widget _buildVerticalDivider() {
|
||||||
return Container(
|
return Container(
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 1,
|
width: 1,
|
||||||
color: AppColor.writeColor.withOpacity(0.2),
|
color: AppColor.writeColor.withValues(alpha: 0.2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- باقي الكود الخاص بك يبقى كما هو بدون تغيير ---
|
|
||||||
|
|
||||||
class TestPage extends StatelessWidget {
|
class TestPage extends StatelessWidget {
|
||||||
const TestPage({super.key});
|
const TestPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final random = Random();
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('iOS Live Activity Test'),
|
title: const Text('iOS Live Activity Test'),
|
||||||
@@ -137,7 +119,6 @@ class TestPage extends StatelessWidget {
|
|||||||
title: 'title',
|
title: 'title',
|
||||||
onPressed: () {},
|
onPressed: () {},
|
||||||
),
|
),
|
||||||
// زر الإنهاء
|
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.red,
|
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/colors.dart';
|
||||||
import '../../../constant/links.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 '../../notification/notification_page.dart';
|
||||||
import '../HomePage/contact_us.dart';
|
import '../HomePage/contact_us.dart';
|
||||||
import '../HomePage/share_app_page.dart';
|
import '../HomePage/share_app_page.dart';
|
||||||
@@ -28,9 +28,8 @@ Color get _kBg =>
|
|||||||
Get.isDarkMode ? const Color(0xFF060B18) : AppColor.secondaryColor;
|
Get.isDarkMode ? const Color(0xFF060B18) : AppColor.secondaryColor;
|
||||||
Color get _kBgSurface => Get.isDarkMode
|
Color get _kBgSurface => Get.isDarkMode
|
||||||
? const Color(0xFF0D1525)
|
? const Color(0xFF0D1525)
|
||||||
: AppColor.secondaryColor.withOpacity(0.9);
|
: AppColor.secondaryColor.withValues(alpha: 0.9);
|
||||||
const _kAmber = Color(0xFFFFB700);
|
const _kAmber = Color(0xFFFFB700);
|
||||||
Color get _kBorder => _kCyan.withOpacity(0.15);
|
|
||||||
Color get _kText => AppColor.writeColor;
|
Color get _kText => AppColor.writeColor;
|
||||||
Color get _kTextMuted => AppColor.grayColor;
|
Color get _kTextMuted => AppColor.grayColor;
|
||||||
|
|
||||||
@@ -39,16 +38,14 @@ class MapMenuWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.lazyPut(() => MapPassengerController());
|
return GetBuilder<MapEngineController>(
|
||||||
|
|
||||||
return GetBuilder<MapPassengerController>(
|
|
||||||
builder: (controller) => Stack(
|
builder: (controller) => Stack(
|
||||||
children: [
|
children: [
|
||||||
// ── تعتيم الخلفية ───────────────────────────────────────────────
|
// ── تعتيم الخلفية ───────────────────────────────────────────────
|
||||||
if (controller.widthMenu > 0)
|
if (controller.widthMenu > 0)
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: controller.getDrawerMenu,
|
onTap: controller.getDrawerMenu,
|
||||||
child: Container(color: Colors.black.withOpacity(0.55)),
|
child: Container(color: Colors.black.withValues(alpha: 0.55)),
|
||||||
),
|
),
|
||||||
|
|
||||||
_buildSideMenu(controller),
|
_buildSideMenu(controller),
|
||||||
@@ -59,7 +56,7 @@ class MapMenuWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── زر القائمة العائم ────────────────────────────────────────────────────
|
// ── زر القائمة العائم ────────────────────────────────────────────────────
|
||||||
Widget _buildMenuButton(MapPassengerController controller) {
|
Widget _buildMenuButton(MapEngineController controller) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: 45,
|
top: 45,
|
||||||
left: 16,
|
left: 16,
|
||||||
@@ -76,12 +73,12 @@ class MapMenuWidget extends StatelessWidget {
|
|||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _kBg.withOpacity(0.88),
|
color: _kBg.withValues(alpha: 0.88),
|
||||||
borderRadius: BorderRadius.circular(16),
|
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: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: _kCyan.withOpacity(0.12),
|
color: _kCyan.withValues(alpha: 0.12),
|
||||||
blurRadius: 16,
|
blurRadius: 16,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -106,7 +103,7 @@ class MapMenuWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── القائمة الجانبية ─────────────────────────────────────────────────────
|
// ── القائمة الجانبية ─────────────────────────────────────────────────────
|
||||||
Widget _buildSideMenu(MapPassengerController controller) {
|
Widget _buildSideMenu(MapEngineController controller) {
|
||||||
return AnimatedPositioned(
|
return AnimatedPositioned(
|
||||||
duration: const Duration(milliseconds: 420),
|
duration: const Duration(milliseconds: 420),
|
||||||
curve: Curves.fastOutSlowIn,
|
curve: Curves.fastOutSlowIn,
|
||||||
@@ -120,13 +117,13 @@ class MapMenuWidget extends StatelessWidget {
|
|||||||
width: Get.width * 0.8,
|
width: Get.width * 0.8,
|
||||||
constraints: const BoxConstraints(maxWidth: 320),
|
constraints: const BoxConstraints(maxWidth: 320),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _kBg.withOpacity(0.97),
|
color: _kBg.withValues(alpha: 0.97),
|
||||||
border: Border(
|
border: Border(
|
||||||
right: BorderSide(color: _kCyan.withOpacity(0.12), width: 1),
|
right: BorderSide(color: _kCyan.withValues(alpha: 0.12), width: 1),
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.5),
|
color: Colors.black.withValues(alpha: 0.5),
|
||||||
blurRadius: 32,
|
blurRadius: 32,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -239,7 +236,7 @@ class MapMenuWidget extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _kBgSurface,
|
color: _kBgSurface,
|
||||||
borderRadius: BorderRadius.circular(16),
|
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(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -255,12 +252,12 @@ class MapMenuWidget extends StatelessWidget {
|
|||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: [
|
||||||
_kCyan.withOpacity(0.2),
|
_kCyan.withValues(alpha: 0.2),
|
||||||
_kAmber.withOpacity(0.12),
|
_kAmber.withValues(alpha: 0.12),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
border:
|
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),
|
child: Icon(Icons.person_rounded, color: _kCyan, size: 28),
|
||||||
),
|
),
|
||||||
@@ -277,7 +274,7 @@ class MapMenuWidget extends StatelessWidget {
|
|||||||
border: Border.all(color: _kBg, width: 2),
|
border: Border.all(color: _kBg, width: 2),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: const Color(0xFF00E676).withOpacity(0.5),
|
color: const Color(0xFF00E676).withValues(alpha: 0.5),
|
||||||
blurRadius: 6,
|
blurRadius: 6,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -365,7 +362,7 @@ class MapMenuWidget extends StatelessWidget {
|
|||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [
|
colors: [
|
||||||
Colors.transparent,
|
Colors.transparent,
|
||||||
_kCyan.withOpacity(0.15),
|
_kCyan.withValues(alpha: 0.15),
|
||||||
Colors.transparent,
|
Colors.transparent,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -419,7 +416,7 @@ class _QuickBtn extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _kBgSurface,
|
color: _kBgSurface,
|
||||||
borderRadius: BorderRadius.circular(12),
|
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(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -463,7 +460,7 @@ class MenuListItem extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final iconColor = isDestructive
|
final iconColor = isDestructive
|
||||||
? const Color(0xFFFF5252)
|
? const Color(0xFFFF5252)
|
||||||
: (color ?? _kCyan.withOpacity(0.80));
|
: (color ?? _kCyan.withValues(alpha: 0.80));
|
||||||
final textColor =
|
final textColor =
|
||||||
isDestructive ? const Color(0xFFFF5252) : (color ?? _kText);
|
isDestructive ? const Color(0xFFFF5252) : (color ?? _kText);
|
||||||
|
|
||||||
@@ -472,8 +469,8 @@ class MenuListItem extends StatelessWidget {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
splashColor: _kCyan.withOpacity(0.07),
|
splashColor: _kCyan.withValues(alpha: 0.07),
|
||||||
highlightColor: _kCyan.withOpacity(0.04),
|
highlightColor: _kCyan.withValues(alpha: 0.04),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -484,8 +481,8 @@ class MenuListItem extends StatelessWidget {
|
|||||||
height: 36,
|
height: 36,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDestructive
|
color: isDestructive
|
||||||
? const Color(0xFFFF5252).withOpacity(0.08)
|
? const Color(0xFFFF5252).withValues(alpha: 0.08)
|
||||||
: _kCyan.withOpacity(0.07),
|
: _kCyan.withValues(alpha: 0.07),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Icon(icon, size: 19, color: iconColor),
|
child: Icon(icon, size: 19, color: iconColor),
|
||||||
@@ -504,7 +501,7 @@ class MenuListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Icon(
|
Icon(
|
||||||
Icons.chevron_right_rounded,
|
Icons.chevron_right_rounded,
|
||||||
color: _kTextMuted.withOpacity(0.4),
|
color: _kTextMuted.withValues(alpha: 0.4),
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -520,7 +517,7 @@ class _MenuGridPainter extends CustomPainter {
|
|||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
final paint = Paint()
|
final paint = Paint()
|
||||||
..color = AppColor.cyanBlue.withOpacity(0.04)
|
..color = AppColor.cyanBlue.withValues(alpha: 0.04)
|
||||||
..strokeWidth = 0.5;
|
..strokeWidth = 0.5;
|
||||||
const spacing = 36.0;
|
const spacing = 36.0;
|
||||||
for (double y = 0; y < size.height; y += spacing) {
|
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/box_name.dart';
|
||||||
import '../../../constant/colors.dart';
|
import '../../../constant/colors.dart';
|
||||||
import '../../../controller/home/map_passenger_controller.dart';
|
import '../../../controller/home/map/map_engine_controller.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
|
|
||||||
class MenuIconMapPageWidget extends StatelessWidget {
|
class MenuIconMapPageWidget extends StatelessWidget {
|
||||||
@@ -13,7 +13,7 @@ class MenuIconMapPageWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<MapEngineController>(
|
||||||
builder: (controller) => Positioned(
|
builder: (controller) => Positioned(
|
||||||
top: Get.height * .008,
|
top: Get.height * .008,
|
||||||
left: box.read(BoxName.lang) != 'ar' ? 5 : null,
|
left: box.read(BoxName.lang) != 'ar' ? 5 : null,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'dart:ui'; // مهم لإضافة تأثير الضبابية
|
|||||||
|
|
||||||
import '../../../constant/colors.dart';
|
import '../../../constant/colors.dart';
|
||||||
import '../../../constant/style.dart';
|
import '../../../constant/style.dart';
|
||||||
import '../../../controller/home/map_passenger_controller.dart';
|
import '../../../controller/home/map/location_search_controller.dart';
|
||||||
|
|
||||||
// --- الويدجت الرئيسية بالتصميم الجديد ---
|
// --- الويدجت الرئيسية بالتصميم الجديد ---
|
||||||
class PassengerRideLocationWidget extends StatefulWidget {
|
class PassengerRideLocationWidget extends StatefulWidget {
|
||||||
@@ -43,7 +43,7 @@ class _PassengerRideLocationWidgetState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
return GetBuilder<LocationSearchController>(builder: (controller) {
|
||||||
// --- نفس شرط الإظهار الخاص بك ---
|
// --- نفس شرط الإظهار الخاص بك ---
|
||||||
return AnimatedPositioned(
|
return AnimatedPositioned(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
@@ -60,9 +60,9 @@ class _PassengerRideLocationWidgetState
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.secondaryColor.withOpacity(0.85),
|
color: AppColor.secondaryColor.withValues(alpha: 0.85),
|
||||||
borderRadius: BorderRadius.circular(50.0),
|
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(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -89,7 +89,7 @@ class _PassengerRideLocationWidgetState
|
|||||||
Text(
|
Text(
|
||||||
"Move the map to adjust the pin".tr,
|
"Move the map to adjust the pin".tr,
|
||||||
style: AppStyle.subtitle.copyWith(
|
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/colors.dart';
|
||||||
import '../../../constant/style.dart';
|
import '../../../constant/style.dart';
|
||||||
import '../../../controller/functions/digit_obsecur_formate.dart';
|
import '../../../controller/home/map/map_engine_controller.dart';
|
||||||
import '../../../controller/home/map_passenger_controller.dart';
|
|
||||||
|
|
||||||
class PaymentMethodPage extends StatelessWidget {
|
class PaymentMethodPage extends StatelessWidget {
|
||||||
const PaymentMethodPage({
|
const PaymentMethodPage({
|
||||||
@@ -18,7 +17,7 @@ class PaymentMethodPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<MapEngineController>(
|
||||||
builder: (controller) => Positioned(
|
builder: (controller) => Positioned(
|
||||||
right: 5,
|
right: 5,
|
||||||
bottom: 5,
|
bottom: 5,
|
||||||
|
|||||||
@@ -4,20 +4,23 @@ import 'package:Intaleq/constant/table_names.dart';
|
|||||||
|
|
||||||
import '../../../constant/colors.dart';
|
import '../../../constant/colors.dart';
|
||||||
import '../../../constant/style.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 '../../../main.dart';
|
||||||
import '../../widgets/elevated_btn.dart';
|
import '../../widgets/elevated_btn.dart';
|
||||||
import 'form_search_places_destenation.dart';
|
import 'form_search_places_destenation.dart';
|
||||||
|
|
||||||
class PickerAnimtionContainerFormPlaces extends StatelessWidget {
|
class PickerAnimtionContainerFormPlaces extends StatelessWidget {
|
||||||
PickerAnimtionContainerFormPlaces({
|
const PickerAnimtionContainerFormPlaces({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
final controller = MapPassengerController();
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// DbSql sql = DbSql.instance;
|
final mapEngine = Get.find<MapEngineController>();
|
||||||
return GetBuilder<MapPassengerController>(
|
final locationSearch = Get.find<LocationSearchController>();
|
||||||
|
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||||
|
|
||||||
|
return GetBuilder<MapEngineController>(
|
||||||
builder: (controller) => Positioned(
|
builder: (controller) => Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -101,8 +104,7 @@ class PickerAnimtionContainerFormPlaces extends StatelessWidget {
|
|||||||
? Center(
|
? Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment
|
MainAxisAlignment.center,
|
||||||
.center,
|
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
Icons
|
Icons
|
||||||
@@ -132,9 +134,9 @@ class PickerAnimtionContainerFormPlaces extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await controller
|
await rideLifecycle
|
||||||
.getDirectionMap(
|
.getDirectionMap(
|
||||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
'${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}',
|
||||||
'${favoritePlaces[index]['latitude']},${favoritePlaces[index]['longitude']}',
|
'${favoritePlaces[index]['latitude']},${favoritePlaces[index]['longitude']}',
|
||||||
);
|
);
|
||||||
controller
|
controller
|
||||||
@@ -143,7 +145,7 @@ class PickerAnimtionContainerFormPlaces extends StatelessWidget {
|
|||||||
.changeBottomSheetShown(
|
.changeBottomSheetShown(
|
||||||
forceValue:
|
forceValue:
|
||||||
true);
|
true);
|
||||||
controller
|
rideLifecycle
|
||||||
.bottomSheet();
|
.bottomSheet();
|
||||||
Get.back();
|
Get.back();
|
||||||
},
|
},
|
||||||
@@ -189,24 +191,22 @@ class PickerAnimtionContainerFormPlaces extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (controller.isPickerShown &&
|
if (controller.isPickerShown &&
|
||||||
controller.placesDestination.isEmpty)
|
locationSearch.placesDestination.isEmpty)
|
||||||
MyElevatedButton(
|
MyElevatedButton(
|
||||||
title: 'Go to this Target'.tr,
|
title: 'Go to this Target'.tr,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await controller.getDirectionMap(
|
await rideLifecycle.getDirectionMap(
|
||||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
'${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}',
|
||||||
'${controller.newMyLocation.latitude},${controller.newMyLocation.longitude}',
|
'${locationSearch.newMyLocation.latitude},${locationSearch.newMyLocation.longitude}',
|
||||||
);
|
);
|
||||||
controller.changePickerShown();
|
controller.changePickerShown();
|
||||||
controller.changeBottomSheetShown(
|
controller.changeBottomSheetShown(
|
||||||
forceValue: true);
|
forceValue: true);
|
||||||
controller.bottomSheet();
|
rideLifecycle.bottomSheet();
|
||||||
// await sql
|
|
||||||
// .getAllData(TableName.placesFavorite)
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (controller.isPickerShown &&
|
if (controller.isPickerShown &&
|
||||||
controller.placesDestination.isEmpty)
|
locationSearch.placesDestination.isEmpty)
|
||||||
const SizedBox(),
|
const SizedBox(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,21 +3,25 @@ import 'package:get/get.dart';
|
|||||||
import 'package:Intaleq/constant/style.dart';
|
import 'package:Intaleq/constant/style.dart';
|
||||||
|
|
||||||
import '../../../constant/colors.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';
|
import '../../../controller/home/points_for_rider_controller.dart';
|
||||||
|
|
||||||
class PointsPageForRider extends StatelessWidget {
|
class PointsPageForRider extends StatelessWidget {
|
||||||
PointsPageForRider({
|
PointsPageForRider({
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
MapPassengerController mapPassengerController =
|
|
||||||
Get.put(MapPassengerController());
|
final locationSearch = Get.find<LocationSearchController>();
|
||||||
|
final mapEngine = Get.find<MapEngineController>();
|
||||||
|
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(WayPointController());
|
Get.find<WayPointController>();
|
||||||
|
|
||||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: 2,
|
bottom: 2,
|
||||||
left: 2,
|
left: 2,
|
||||||
@@ -34,7 +38,7 @@ class PointsPageForRider extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
mapPassengerController.downPoints();
|
mapEngine.downPoints();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.arrow_drop_down_circle_outlined),
|
icon: const Icon(Icons.arrow_drop_down_circle_outlined),
|
||||||
),
|
),
|
||||||
@@ -52,7 +56,7 @@ class PointsPageForRider extends StatelessWidget {
|
|||||||
wayPointController.wayPoints.length > 1
|
wayPointController.wayPoints.length > 1
|
||||||
? ElevatedButton(
|
? ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
mapPassengerController
|
locationSearch
|
||||||
.getMapPointsForAllMethods();
|
.getMapPointsForAllMethods();
|
||||||
},
|
},
|
||||||
child: const Text('Get Direction'),
|
child: const Text('Get Direction'),
|
||||||
@@ -74,7 +78,6 @@ class PointsPageForRider extends StatelessWidget {
|
|||||||
.entries
|
.entries
|
||||||
.map((entry) {
|
.map((entry) {
|
||||||
final index = entry.key;
|
final index = entry.key;
|
||||||
final wayPoint = entry.value;
|
|
||||||
return Padding(
|
return Padding(
|
||||||
key: ValueKey(index),
|
key: ValueKey(index),
|
||||||
padding: const EdgeInsets.all(1),
|
padding: const EdgeInsets.all(1),
|
||||||
@@ -98,7 +101,7 @@ class PointsPageForRider extends StatelessWidget {
|
|||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: Get.width,
|
width: Get.width,
|
||||||
height: 400,
|
height: 400,
|
||||||
child: mapPassengerController
|
child: locationSearch
|
||||||
.placeListResponse[index]),
|
.placeListResponse[index]),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -106,13 +109,13 @@ class PointsPageForRider extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(),
|
border: Border.all(),
|
||||||
color:
|
color:
|
||||||
AppColor.accentColor.withOpacity(.5)),
|
AppColor.accentColor.withValues(alpha: 0.5)),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment:
|
||||||
MainAxisAlignment.spaceBetween,
|
MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(index > 0
|
Text(index > 0
|
||||||
? mapPassengerController
|
? locationSearch
|
||||||
.currentLocationStringAll[index]
|
.currentLocationStringAll[index]
|
||||||
.toString()
|
.toString()
|
||||||
: ''),
|
: ''),
|
||||||
@@ -239,7 +242,6 @@ class PointsPageForRider extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void showAddLocationDialog(BuildContext context, int index) {
|
void showAddLocationDialog(BuildContext context, int index) {
|
||||||
final TextEditingController locationController = TextEditingController();
|
|
||||||
// Get.put(WayPointController());
|
// Get.put(WayPointController());
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -298,26 +300,24 @@ class AppBarPointsPageForRider extends StatelessWidget {
|
|||||||
color: AppColor.primaryColor,
|
color: AppColor.primaryColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
CircleAvatar(
|
||||||
CircleAvatar(
|
backgroundColor: AppColor.primaryColor,
|
||||||
backgroundColor: AppColor.primaryColor,
|
maxRadius: 15,
|
||||||
maxRadius: 15,
|
child: Icon(
|
||||||
child: Icon(
|
Icons.person,
|
||||||
Icons.person,
|
color: AppColor.secondaryColor,
|
||||||
color: AppColor.secondaryColor,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
TextButton(
|
),
|
||||||
onPressed: () {},
|
TextButton(
|
||||||
child: Text(
|
onPressed: () {},
|
||||||
"Switch Rider".tr,
|
child: Text(
|
||||||
style: AppStyle.title,
|
"Switch Rider".tr,
|
||||||
),
|
style: AppStyle.title,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
Icon(
|
Icon(
|
||||||
Icons.clear,
|
Icons.clear,
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import '../../../constant/style.dart';
|
|||||||
import '../../../controller/functions/audio_record1.dart';
|
import '../../../controller/functions/audio_record1.dart';
|
||||||
import '../../../controller/functions/launch.dart';
|
import '../../../controller/functions/launch.dart';
|
||||||
import '../../../controller/functions/toast.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 '../../../controller/profile/profile_controller.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../../views/home/profile/complaint_page.dart';
|
import '../../../views/home/profile/complaint_page.dart';
|
||||||
@@ -24,9 +26,10 @@ class RideBeginPassenger extends StatelessWidget {
|
|||||||
final ProfileController profileController = Get.put(ProfileController());
|
final ProfileController profileController = Get.put(ProfileController());
|
||||||
final AudioRecorderController audioController =
|
final AudioRecorderController audioController =
|
||||||
Get.put(AudioRecorderController());
|
Get.put(AudioRecorderController());
|
||||||
|
final uiController = Get.find<UiInteractionsController>();
|
||||||
|
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final controller = Get.find<MapPassengerController>();
|
final controller = Get.find<RideLifecycleController>();
|
||||||
|
|
||||||
// شرط الإظهار
|
// شرط الإظهار
|
||||||
final bool isVisible =
|
final bool isVisible =
|
||||||
@@ -50,8 +53,8 @@ class RideBeginPassenger extends StatelessWidget {
|
|||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Get.isDarkMode
|
color: Get.isDarkMode
|
||||||
? Colors.black.withOpacity(0.4)
|
? Colors.black.withValues(alpha: 0.4)
|
||||||
: Colors.black.withOpacity(0.1),
|
: Colors.black.withValues(alpha: 0.1),
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
spreadRadius: 2,
|
spreadRadius: 2,
|
||||||
offset: const Offset(0, -3),
|
offset: const Offset(0, -3),
|
||||||
@@ -69,7 +72,7 @@ class RideBeginPassenger extends StatelessWidget {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 4,
|
height: 4,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.grayColor.withOpacity(0.3),
|
color: AppColor.grayColor.withValues(alpha: 0.3),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -85,7 +88,7 @@ class RideBeginPassenger extends StatelessWidget {
|
|||||||
Divider(
|
Divider(
|
||||||
height: 1,
|
height: 1,
|
||||||
thickness: 0.5,
|
thickness: 0.5,
|
||||||
color: AppColor.grayColor.withOpacity(0.2)),
|
color: AppColor.grayColor.withValues(alpha: 0.2)),
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
@@ -104,7 +107,7 @@ class RideBeginPassenger extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- الهيدر (بدون تغيير، ممتاز) ---
|
// --- الهيدر (بدون تغيير، ممتاز) ---
|
||||||
Widget _buildCompactHeader(MapPassengerController controller) {
|
Widget _buildCompactHeader(RideLifecycleController controller) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
// صورة السائق
|
// صورة السائق
|
||||||
@@ -112,7 +115,7 @@ class RideBeginPassenger extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: AppColor.primaryColor.withOpacity(0.5), width: 1.5),
|
color: AppColor.primaryColor.withValues(alpha: 0.5), width: 1.5),
|
||||||
),
|
),
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 24,
|
radius: 24,
|
||||||
@@ -166,9 +169,9 @@ class RideBeginPassenger extends StatelessWidget {
|
|||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.writeColor.withOpacity(0.05),
|
color: AppColor.writeColor.withValues(alpha: 0.05),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: AppColor.grayColor.withOpacity(0.2)),
|
color: AppColor.grayColor.withValues(alpha: 0.2)),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -190,7 +193,7 @@ class RideBeginPassenger extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.primaryColor.withOpacity(0.08),
|
color: AppColor.primaryColor.withValues(alpha: 0.08),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -216,9 +219,10 @@ class RideBeginPassenger extends StatelessWidget {
|
|||||||
// --- الأزرار (بدون تغيير) ---
|
// --- الأزرار (بدون تغيير) ---
|
||||||
Widget _buildCompactActionButtons(
|
Widget _buildCompactActionButtons(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MapPassengerController controller,
|
RideLifecycleController controller,
|
||||||
ProfileController profileController,
|
ProfileController profileController,
|
||||||
AudioRecorderController audioController) {
|
AudioRecorderController audioController) {
|
||||||
|
final uiController = Get.find<UiInteractionsController>();
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 60,
|
height: 60,
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -228,7 +232,7 @@ class RideBeginPassenger extends StatelessWidget {
|
|||||||
icon: Icons.sos_rounded,
|
icon: Icons.sos_rounded,
|
||||||
label: 'SOS'.tr,
|
label: 'SOS'.tr,
|
||||||
color: AppColor.redColor,
|
color: AppColor.redColor,
|
||||||
bgColor: AppColor.redColor.withOpacity(0.1),
|
bgColor: AppColor.redColor.withValues(alpha: 0.1),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (box.read(BoxName.sosPhonePassenger) == null) {
|
if (box.read(BoxName.sosPhonePassenger) == null) {
|
||||||
await profileController.updatField(
|
await profileController.updatField(
|
||||||
@@ -244,24 +248,26 @@ class RideBeginPassenger extends StatelessWidget {
|
|||||||
icon: FontAwesome.whatsapp,
|
icon: FontAwesome.whatsapp,
|
||||||
label: 'WhatsApp'.tr,
|
label: 'WhatsApp'.tr,
|
||||||
color: const Color(0xFF25D366),
|
color: const Color(0xFF25D366),
|
||||||
bgColor: const Color(0xFF25D366).withOpacity(0.1),
|
bgColor: const Color(0xFF25D366).withValues(alpha: 0.1),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (box.read(BoxName.sosPhonePassenger) == null) {
|
final phone = box.read(BoxName.sosPhonePassenger);
|
||||||
await profileController.updatField(
|
if (phone == null || phone.toString().isEmpty) {
|
||||||
'sosPhone', TextInputType.phone);
|
// لا يوجد رقم طوارئ — نعرض الديالوج لإدخاله
|
||||||
|
await uiController.shareTripWithFamily();
|
||||||
} else {
|
} else {
|
||||||
final phone = controller.formatSyrianPhoneNumber(
|
final formattedPhone = uiController.formatSyrianPhoneNumber(
|
||||||
box.read(BoxName.sosPhonePassenger).toString());
|
phone.toString());
|
||||||
controller.sendWhatsapp(phone);
|
uiController.sendWhatsapp(formattedPhone);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
_compactBtn(
|
_compactBtn(
|
||||||
icon: Icons.share,
|
icon: Icons.share,
|
||||||
label: 'Share'.tr,
|
label: 'Share'.tr,
|
||||||
color: AppColor.primaryColor,
|
color: AppColor.primaryColor,
|
||||||
bgColor: AppColor.primaryColor.withOpacity(0.1),
|
bgColor: AppColor.primaryColor.withValues(alpha: 0.1),
|
||||||
onTap: () async => await controller.shareTripWithFamily(),
|
onTap: () async => await uiController.shareTripWithFamily(),
|
||||||
),
|
),
|
||||||
GetBuilder<AudioRecorderController>(
|
GetBuilder<AudioRecorderController>(
|
||||||
init: audioController,
|
init: audioController,
|
||||||
@@ -275,15 +281,19 @@ class RideBeginPassenger extends StatelessWidget {
|
|||||||
? AppColor.redColor
|
? AppColor.redColor
|
||||||
: AppColor.primaryColor,
|
: AppColor.primaryColor,
|
||||||
bgColor: audioCtx.isRecording
|
bgColor: audioCtx.isRecording
|
||||||
? AppColor.redColor.withOpacity(0.1)
|
? AppColor.redColor.withValues(alpha: 0.1)
|
||||||
: AppColor.primaryColor.withOpacity(0.1),
|
: AppColor.primaryColor.withValues(alpha: 0.1),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (!audioCtx.isRecording) {
|
if (!audioCtx.isRecording) {
|
||||||
await audioCtx.startRecording();
|
await audioCtx.startRecording(rideId: controller.rideId);
|
||||||
Toast.show(context, 'Start Record'.tr, AppColor.greenColor);
|
if (context.mounted) {
|
||||||
|
Toast.show(context, 'Start Record'.tr, AppColor.greenColor);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await audioCtx.stopRecording();
|
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,
|
icon: Icons.info_outline_rounded,
|
||||||
label: 'Report'.tr,
|
label: 'Report'.tr,
|
||||||
color: AppColor.grayColor,
|
color: AppColor.grayColor,
|
||||||
bgColor: AppColor.writeColor.withOpacity(0.1),
|
bgColor: AppColor.writeColor.withValues(alpha: 0.1),
|
||||||
onTap: () => Get.to(() => ComplaintPage()),
|
onTap: () => Get.to(() => ComplaintPage()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import '../../../constant/box_name.dart';
|
|||||||
import '../../../constant/colors.dart';
|
import '../../../constant/colors.dart';
|
||||||
import '../../../constant/links.dart';
|
import '../../../constant/links.dart';
|
||||||
import '../../../constant/style.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 '../../../controller/profile/profile_controller.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
|
|
||||||
@@ -18,8 +20,10 @@ class RideFromStartApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final profileController = Get.put(ProfileController());
|
final profileController = Get.put(ProfileController());
|
||||||
final MapPassengerController controller =
|
final RideLifecycleController controller =
|
||||||
Get.find<MapPassengerController>();
|
Get.find<RideLifecycleController>();
|
||||||
|
final UiInteractionsController uiController =
|
||||||
|
Get.find<UiInteractionsController>();
|
||||||
|
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final bool isRideActive =
|
final bool isRideActive =
|
||||||
@@ -59,7 +63,7 @@ class RideFromStartApp extends StatelessWidget {
|
|||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Get.isDarkMode
|
color: Get.isDarkMode
|
||||||
? Colors.black.withOpacity(0.4)
|
? Colors.black.withValues(alpha: 0.4)
|
||||||
: Colors.black12,
|
: Colors.black12,
|
||||||
blurRadius: 15.0,
|
blurRadius: 15.0,
|
||||||
spreadRadius: 5.0,
|
spreadRadius: 5.0,
|
||||||
@@ -78,7 +82,7 @@ class RideFromStartApp extends StatelessWidget {
|
|||||||
height: 4,
|
height: 4,
|
||||||
margin: const EdgeInsets.only(bottom: 15),
|
margin: const EdgeInsets.only(bottom: 15),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.grayColor.withOpacity(0.3),
|
color: AppColor.grayColor.withValues(alpha: 0.3),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -134,7 +138,7 @@ class RideFromStartApp extends StatelessWidget {
|
|||||||
Container(
|
Container(
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 12,
|
height: 12,
|
||||||
color: AppColor.grayColor.withOpacity(0.3)),
|
color: AppColor.grayColor.withValues(alpha: 0.3)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
"$carType - $carModel",
|
"$carType - $carModel",
|
||||||
@@ -160,7 +164,7 @@ class RideFromStartApp extends StatelessWidget {
|
|||||||
const EdgeInsets.symmetric(vertical: 12, horizontal: 10),
|
const EdgeInsets.symmetric(vertical: 12, horizontal: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.grayColor
|
color: AppColor.grayColor
|
||||||
.withOpacity(0.1), // خلفية رمادية خفيفة جداً
|
.withValues(alpha: 0.1), // خلفية رمادية خفيفة جداً
|
||||||
borderRadius: BorderRadius.circular(15),
|
borderRadius: BorderRadius.circular(15),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -188,7 +192,7 @@ class RideFromStartApp extends StatelessWidget {
|
|||||||
flex: 2,
|
flex: 2,
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () => _checkAndCall(
|
onPressed: () => _checkAndCall(
|
||||||
controller.sendWhatsapp, profileController),
|
uiController.sendWhatsapp, profileController),
|
||||||
icon:
|
icon:
|
||||||
const Icon(FontAwesome.whatsapp, color: Colors.white),
|
const Icon(FontAwesome.whatsapp, color: Colors.white),
|
||||||
label: Text("Share Trip".tr,
|
label: Text("Share Trip".tr,
|
||||||
@@ -252,9 +256,9 @@ class RideFromStartApp extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withOpacity(0.1),
|
color: color.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: color.withOpacity(0.5)),
|
border: Border.all(color: color.withValues(alpha: 0.5)),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
@@ -297,7 +301,7 @@ class RideFromStartApp extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
height: 30,
|
height: 30,
|
||||||
width: 1,
|
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:get/get.dart';
|
||||||
import 'package:Intaleq/constant/colors.dart';
|
import 'package:Intaleq/constant/colors.dart';
|
||||||
import 'package:Intaleq/constant/style.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 {
|
class SearchingCaptainWindow extends StatefulWidget {
|
||||||
@@ -36,7 +37,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
|||||||
// [تعديل 1] نستخدم Obx للاستماع إلى التغييرات في حالة الرحلة
|
// [تعديل 1] نستخدم Obx للاستماع إلى التغييرات في حالة الرحلة
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
// ابحث عن الكنترولر مرة واحدة
|
// ابحث عن الكنترولر مرة واحدة
|
||||||
final controller = Get.find<MapPassengerController>();
|
final controller = Get.find<RideLifecycleController>();
|
||||||
|
|
||||||
// [تعديل 2] شرط الإظهار يعتمد الآن على حالة الرحلة مباشرة
|
// [تعديل 2] شرط الإظهار يعتمد الآن على حالة الرحلة مباشرة
|
||||||
final bool isVisible =
|
final bool isVisible =
|
||||||
@@ -58,7 +59,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
|||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.2),
|
color: Colors.black.withValues(alpha: 0.2),
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
offset: const Offset(0, -5),
|
offset: const Offset(0, -5),
|
||||||
),
|
),
|
||||||
@@ -83,7 +84,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
|||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: AppColor.writeColor,
|
foregroundColor: AppColor.writeColor,
|
||||||
side:
|
side:
|
||||||
BorderSide(color: AppColor.writeColor.withOpacity(0.3)),
|
BorderSide(color: AppColor.writeColor.withValues(alpha: 0.3)),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 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(
|
return SizedBox(
|
||||||
height: 180, // ارتفاع ثابت لمنطقة الأنيميشن
|
height: 180, // ارتفاع ثابت لمنطقة الأنيميشن
|
||||||
child: Stack(
|
child: Stack(
|
||||||
@@ -125,7 +126,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: AppColor.primaryColor.withOpacity(0.7),
|
color: AppColor.primaryColor.withValues(alpha: 0.7),
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -147,7 +148,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
|||||||
Text(
|
Text(
|
||||||
'Searching for the nearest captain...'.tr,
|
'Searching for the nearest captain...'.tr,
|
||||||
style: AppStyle.subtitle
|
style: AppStyle.subtitle
|
||||||
.copyWith(color: AppColor.writeColor.withOpacity(0.7)),
|
.copyWith(color: AppColor.writeColor.withValues(alpha: 0.7)),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@@ -166,12 +167,12 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
|
|||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
color: AppColor.primaryColor,
|
color: AppColor.primaryColor,
|
||||||
backgroundColor: AppColor.primaryColor.withOpacity(0.2),
|
backgroundColor: AppColor.primaryColor.withValues(alpha: 0.2),
|
||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.search,
|
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/colors.dart';
|
||||||
import 'package:Intaleq/constant/style.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:Intaleq/views/widgets/elevated_btn.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -10,8 +10,10 @@ import '../../../constant/links.dart';
|
|||||||
import '../../../print.dart';
|
import '../../../print.dart';
|
||||||
|
|
||||||
class CupertinoDriverListWidget extends StatelessWidget {
|
class CupertinoDriverListWidget extends StatelessWidget {
|
||||||
MapPassengerController mapPassengerController =
|
CupertinoDriverListWidget({super.key});
|
||||||
Get.put(MapPassengerController());
|
|
||||||
|
final RideLifecycleController mapPassengerController =
|
||||||
|
Get.find<RideLifecycleController>();
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CupertinoPageScaffold(
|
return CupertinoPageScaffold(
|
||||||
@@ -158,7 +160,7 @@ class CupertinoDriverListWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Log.print(' driver["id"]: ${driver['driver_id']}');
|
Log.print(' driver["id"]: ${driver['driver_id']}');
|
||||||
Get.find<MapPassengerController>().driverIdVip =
|
Get.find<RideLifecycleController>().driverIdVip =
|
||||||
driver['driver_id'];
|
driver['driver_id'];
|
||||||
|
|
||||||
// Handle driver selection
|
// Handle driver selection
|
||||||
@@ -266,8 +268,6 @@ class CupertinoDriverListWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void showDateTimePickerDialog(Map<String, dynamic> driver) {
|
void showDateTimePickerDialog(Map<String, dynamic> driver) {
|
||||||
DateTime selectedDateTime = DateTime.now();
|
|
||||||
|
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
title: "Select date and time of trip".tr,
|
title: "Select date and time of trip".tr,
|
||||||
@@ -302,7 +302,9 @@ class CupertinoDriverListWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DateTimePickerWidget extends StatelessWidget {
|
class DateTimePickerWidget extends StatelessWidget {
|
||||||
final MapPassengerController controller = Get.put(MapPassengerController());
|
DateTimePickerWidget({super.key});
|
||||||
|
|
||||||
|
final RideLifecycleController controller = Get.find<RideLifecycleController>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../../constant/style.dart';
|
import '../../../constant/style.dart';
|
||||||
import '../../../controller/home/map_passenger_controller.dart';
|
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||||
|
|
||||||
GetBuilder<MapPassengerController> timerForCancelTripFromPassenger() {
|
GetBuilder<RideLifecycleController> timerForCancelTripFromPassenger() {
|
||||||
return GetBuilder<MapPassengerController>(
|
return GetBuilder<RideLifecycleController>(
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
final isNearEnd =
|
final isNearEnd =
|
||||||
controller.remainingTime <= 5; // Define a threshold for "near end"
|
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/colors.dart';
|
||||||
import '../../../constant/style.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';
|
import 'ride_begin_passenger.dart';
|
||||||
|
|
||||||
class TimerToPassengerFromDriver extends StatelessWidget {
|
class TimerToPassengerFromDriver extends StatelessWidget {
|
||||||
@@ -14,7 +14,7 @@ class TimerToPassengerFromDriver extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||||
if (controller.remainingTime == 0 &&
|
if (controller.remainingTime == 0 &&
|
||||||
(controller.isDriverInPassengerWay == true ||
|
(controller.isDriverInPassengerWay == true ||
|
||||||
controller.timeToPassengerFromDriverAfterApplied > 0)) {
|
controller.timeToPassengerFromDriverAfterApplied > 0)) {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import '../../../constant/style.dart';
|
|||||||
import '../../../controller/functions/audio_record1.dart';
|
import '../../../controller/functions/audio_record1.dart';
|
||||||
import '../../../controller/functions/launch.dart';
|
import '../../../controller/functions/launch.dart';
|
||||||
import '../../../controller/functions/toast.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 {
|
class VipRideBeginPassenger extends StatelessWidget {
|
||||||
const VipRideBeginPassenger({
|
const VipRideBeginPassenger({
|
||||||
@@ -24,8 +25,8 @@ class VipRideBeginPassenger extends StatelessWidget {
|
|||||||
ProfileController profileController = Get.put(ProfileController());
|
ProfileController profileController = Get.put(ProfileController());
|
||||||
AudioRecorderController audioController =
|
AudioRecorderController audioController =
|
||||||
Get.put(AudioRecorderController());
|
Get.put(AudioRecorderController());
|
||||||
// Get.put(MapPassengerController());
|
final uiController = Get.find<UiInteractionsController>();
|
||||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||||
if (controller.statusRideVip == 'Begin' ||
|
if (controller.statusRideVip == 'Begin' ||
|
||||||
!controller.statusRideFromStart) {
|
!controller.statusRideFromStart) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
@@ -148,9 +149,11 @@ class VipRideBeginPassenger extends StatelessWidget {
|
|||||||
child: audioController.isRecording == false
|
child: audioController.isRecording == false
|
||||||
? IconButton(
|
? IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await audioController.startRecording();
|
await audioController.startRecording(rideId: controller.rideId);
|
||||||
Toast.show(context, 'Start Record'.tr,
|
if (context.mounted) {
|
||||||
AppColor.greenColor);
|
Toast.show(context, 'Start Record'.tr,
|
||||||
|
AppColor.greenColor);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.play_circle_fill_outlined,
|
Icons.play_circle_fill_outlined,
|
||||||
@@ -162,8 +165,10 @@ class VipRideBeginPassenger extends StatelessWidget {
|
|||||||
: IconButton(
|
: IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await audioController.stopRecording();
|
await audioController.stopRecording();
|
||||||
Toast.show(context, 'Record saved'.tr,
|
if (context.mounted) {
|
||||||
AppColor.greenColor);
|
Toast.show(context, 'Record saved'.tr,
|
||||||
|
AppColor.greenColor);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.stop_circle,
|
Icons.stop_circle,
|
||||||
@@ -215,15 +220,11 @@ class VipRideBeginPassenger extends StatelessWidget {
|
|||||||
profileController.prfoileData['sosPhone']);
|
profileController.prfoileData['sosPhone']);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String phoneNumber = box
|
|
||||||
.read(BoxName.sosPhonePassenger)
|
|
||||||
.toString();
|
|
||||||
// phoneNumber = phoneNumber.replaceAll('0', '');
|
|
||||||
var phone = box.read(BoxName.countryCode) ==
|
var phone = box.read(BoxName.countryCode) ==
|
||||||
'Egypt'
|
'Egypt'
|
||||||
? '+2${box.read(BoxName.sosPhonePassenger)}'
|
? '+2${box.read(BoxName.sosPhonePassenger)}'
|
||||||
: '+962${box.read(BoxName.sosPhonePassenger)}';
|
: '+962${box.read(BoxName.sosPhonePassenger)}';
|
||||||
controller.sendWhatsapp(phone);
|
uiController.sendWhatsapp(phone);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
@@ -237,7 +238,7 @@ class VipRideBeginPassenger extends StatelessWidget {
|
|||||||
width: Get.width * .15,
|
width: Get.width * .15,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await controller.shareTripWithFamily();
|
await uiController.shareTripWithFamily();
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
AntDesign.Safety,
|
AntDesign.Safety,
|
||||||
@@ -283,13 +284,12 @@ class VipRideBeginPassenger extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class StreamCounter extends StatelessWidget {
|
class StreamCounter extends StatelessWidget {
|
||||||
const StreamCounter({Key? key}) : super(key: key);
|
const StreamCounter({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// Build the UI based on the timer value
|
// Build the UI based on the timer value
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(MapPassengerController());
|
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||||
return GetBuilder<MapPassengerController>(builder: (controller) {
|
|
||||||
return StreamBuilder<int>(
|
return StreamBuilder<int>(
|
||||||
initialData: 0,
|
initialData: 0,
|
||||||
stream: controller.timerController.stream,
|
stream: controller.timerController.stream,
|
||||||
|
|||||||
@@ -268,9 +268,6 @@ class NavigationController extends GetxController
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
static final String _routeApiBaseUrl =
|
|
||||||
"${AppLink.routesOsm}/route/v1/driving";
|
|
||||||
|
|
||||||
IconData get currentManeuverIcon {
|
IconData get currentManeuverIcon {
|
||||||
switch (currentManeuverModifier) {
|
switch (currentManeuverModifier) {
|
||||||
case 4: // Arrive
|
case 4: // Arrive
|
||||||
@@ -378,7 +375,6 @@ class NavigationController extends GetxController
|
|||||||
void onMapCreated(IntaleqMapController controller) async {
|
void onMapCreated(IntaleqMapController controller) async {
|
||||||
Log.print("DEBUG: NavigationController.onMapCreated called");
|
Log.print("DEBUG: NavigationController.onMapCreated called");
|
||||||
mapController = controller;
|
mapController = controller;
|
||||||
await onStyleLoaded();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onStyleLoaded() async {
|
Future<void> onStyleLoaded() async {
|
||||||
@@ -577,7 +573,7 @@ class NavigationController extends GetxController
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _checkOffRoute(LatLng pos) {
|
void _checkOffRoute(LatLng pos) {
|
||||||
if (_autoRecalcInProgress || isLoading) return;
|
if (!isNavigating || _autoRecalcInProgress || isLoading) return;
|
||||||
if (_fullRouteCoordinates.isEmpty) return;
|
if (_fullRouteCoordinates.isEmpty) return;
|
||||||
|
|
||||||
const int searchWindow = 80;
|
const int searchWindow = 80;
|
||||||
@@ -604,7 +600,6 @@ class NavigationController extends GetxController
|
|||||||
if (elapsed >= _offRouteTriggerSeconds) {
|
if (elapsed >= _offRouteTriggerSeconds) {
|
||||||
_offRouteStartTime = null;
|
_offRouteStartTime = null;
|
||||||
_autoRecalcInProgress = true;
|
_autoRecalcInProgress = true;
|
||||||
// Smart reroute: check if we have alternative routes available
|
|
||||||
_smartRecalculateRoute(pos);
|
_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 {
|
Future<void> _smartRecalculateRoute(LatLng currentPos) async {
|
||||||
try {
|
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) {
|
if (_finalDestination != null) {
|
||||||
await recalculateRoute();
|
await recalculateRoute(origin: currentPos, keepNavigationActive: true);
|
||||||
}
|
}
|
||||||
_autoRecalcInProgress = false;
|
_autoRecalcInProgress = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -906,7 +872,8 @@ class NavigationController extends GetxController
|
|||||||
return const LatLng(31.7225, 35.9933); // Queen Alia Airport (JO)
|
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;
|
isLoading = true;
|
||||||
update();
|
update();
|
||||||
|
|
||||||
@@ -1007,9 +974,8 @@ class NavigationController extends GetxController
|
|||||||
currentStepIndex = 0;
|
currentStepIndex = 0;
|
||||||
_nextInstructionSpoken = false;
|
_nextInstructionSpoken = false;
|
||||||
|
|
||||||
// Don't start navigating immediately, wait for user to press Start
|
isNavigating = keepNavigationActive;
|
||||||
isNavigating = false;
|
_cameraLockedToUser = keepNavigationActive;
|
||||||
_cameraLockedToUser = false;
|
|
||||||
_offRouteStartTime = null;
|
_offRouteStartTime = null;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|
||||||
@@ -1032,7 +998,10 @@ class NavigationController extends GetxController
|
|||||||
// Re-add car marker after polyline updates (ensures it stays on top)
|
// Re-add car marker after polyline updates (ensures it stays on top)
|
||||||
if (isStyleLoaded) _updateCarMarker();
|
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 =
|
final bounds =
|
||||||
data['bbox'] != null && (data['bbox'] as List).length == 4
|
data['bbox'] != null && (data['bbox'] as List).length == 4
|
||||||
? LatLngBounds(
|
? LatLngBounds(
|
||||||
@@ -1117,12 +1086,23 @@ class NavigationController extends GetxController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> recalculateRoute() async {
|
Future<void> recalculateRoute(
|
||||||
if (myLocation == null || _finalDestination == null || isLoading) return;
|
{LatLng? origin, bool keepNavigationActive = false}) async {
|
||||||
|
final LatLng? routeOrigin = origin ?? myLocation;
|
||||||
|
if (routeOrigin == null || _finalDestination == null || isLoading) return;
|
||||||
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
update();
|
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;
|
isLoading = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
@@ -1290,17 +1270,19 @@ class NavigationController extends GetxController
|
|||||||
try {
|
try {
|
||||||
// ✅ Use searchPlaces from intaleq_maps SDK
|
// ✅ Use searchPlaces from intaleq_maps SDK
|
||||||
final results = await mapController!.searchPlaces(q);
|
final results = await mapController!.searchPlaces(q);
|
||||||
|
|
||||||
if (myLocation != null) {
|
if (myLocation != null) {
|
||||||
for (final p in results) {
|
for (final p in results) {
|
||||||
final plat = double.tryParse(p['latitude']?.toString() ?? '0') ?? 0.0;
|
final plat = double.tryParse(p['latitude']?.toString() ?? '0') ?? 0.0;
|
||||||
final plng = double.tryParse(p['longitude']?.toString() ?? '0') ?? 0.0;
|
final plng =
|
||||||
p['distanceKm'] = _haversineKm(myLocation!.latitude, myLocation!.longitude, plat, plng);
|
double.tryParse(p['longitude']?.toString() ?? '0') ?? 0.0;
|
||||||
|
p['distanceKm'] = _haversineKm(
|
||||||
|
myLocation!.latitude, myLocation!.longitude, plat, plng);
|
||||||
}
|
}
|
||||||
results.sort((a, b) =>
|
results.sort((a, b) =>
|
||||||
(a['distanceKm'] as double).compareTo(b['distanceKm'] as double));
|
(a['distanceKm'] as double).compareTo(b['distanceKm'] as double));
|
||||||
}
|
}
|
||||||
|
|
||||||
placesDestination = results;
|
placesDestination = results;
|
||||||
update();
|
update();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class NavigationView extends StatelessWidget {
|
|||||||
IntaleqMap(
|
IntaleqMap(
|
||||||
apiKey: Env.mapSaasKey,
|
apiKey: Env.mapSaasKey,
|
||||||
onMapCreated: c.onMapCreated,
|
onMapCreated: c.onMapCreated,
|
||||||
|
onStyleLoaded: c.onStyleLoaded,
|
||||||
onLongPress: (pos) => c.onMapLongPressed(Point(0, 0), pos),
|
onLongPress: (pos) => c.onMapLongPressed(Point(0, 0), pos),
|
||||||
onTap: (pos) => c.onMapTapped(Point(0, 0), pos),
|
onTap: (pos) => c.onMapTapped(Point(0, 0), pos),
|
||||||
markers: c.markers,
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,6 @@ import 'package:flutter_spinkit/flutter_spinkit.dart';
|
|||||||
import 'package:webview_flutter/webview_flutter.dart';
|
import 'package:webview_flutter/webview_flutter.dart';
|
||||||
// Import for Android features.
|
// Import for Android features.
|
||||||
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
import 'package:webview_flutter_android/webview_flutter_android.dart';
|
||||||
// Import for iOS features.
|
|
||||||
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
|
|
||||||
|
|
||||||
import 'src/PaypalServices.dart';
|
import 'src/PaypalServices.dart';
|
||||||
import 'src/errors/network_error.dart';
|
import 'src/errors/network_error.dart';
|
||||||
@@ -128,20 +126,10 @@ class UsePaypalState extends State<UsePaypal> {
|
|||||||
// Enable hybrid composition.
|
// Enable hybrid composition.
|
||||||
loadPayment();
|
loadPayment();
|
||||||
|
|
||||||
// #docregion platform_features
|
const PlatformWebViewControllerCreationParams params =
|
||||||
late final PlatformWebViewControllerCreationParams params;
|
PlatformWebViewControllerCreationParams();
|
||||||
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
|
|
||||||
params = WebKitWebViewControllerCreationParams(
|
|
||||||
allowsInlineMediaPlayback: true,
|
|
||||||
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
params = const PlatformWebViewControllerCreationParams();
|
|
||||||
}
|
|
||||||
|
|
||||||
final WebViewController controller =
|
final WebViewController controller =
|
||||||
WebViewController.fromPlatformCreationParams(params);
|
WebViewController.fromPlatformCreationParams(params);
|
||||||
// #enddocregion platform_features
|
|
||||||
|
|
||||||
controller
|
controller
|
||||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||||
|
|||||||
701
plan/intaleq_admin_analysis_report.md
Normal file
701
plan/intaleq_admin_analysis_report.md
Normal file
@@ -0,0 +1,701 @@
|
|||||||
|
<div dir="rtl" align="right">
|
||||||
|
|
||||||
|
# تقرير تحليل شامل لتطبيق إنتلق - لوحة الإدارة (Intaleq Admin)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 فهرس المحتويات
|
||||||
|
|
||||||
|
1. [نظرة عامة على النظام](#1-نظرة-عامة-على-النظام)
|
||||||
|
2. [هيكل المشاريع والهندسة المعمارية](#2-هيكل-المشاريع-والهندسة-المعمارية)
|
||||||
|
3. [تحليل الباك إند - الخادم الرئيسي (intaleq_v1)](#3-تحليل-الباك-إند---الخادم-الرئيسي)
|
||||||
|
4. [تحليل لوحة الإدارة (intaleq_admin)](#4-تحليل-لوحة-الإدارة)
|
||||||
|
5. [تحليل تطبيق خدمة العملاء (service_intaleq)](#5-تحليل-تطبيق-خدمة-العملاء)
|
||||||
|
6. [قاعدة البيانات والموديلات](#6-قاعدة-البيانات-والموديلات)
|
||||||
|
7. [نظام المصادقة والصلاحيات](#7-نظام-المصادقة-والصلاحيات)
|
||||||
|
8. [تحليل التوافقية بين الباك إند والأدمن](#8-تحليل-التوافقية-بين-الباك-إند-والأدمن)
|
||||||
|
9. [الإضافات والتحسينات المقترحة](#9-الإضافات-والتحسينات-المقترحة)
|
||||||
|
10. [ملخص وتوصيات](#10-ملخص-وتوصيات)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. نظرة عامة على النظام
|
||||||
|
|
||||||
|
### 1.1 وصف النظام
|
||||||
|
|
||||||
|
نظام **إنتلق (Intaleq)** هو منصة متكاملة لخدمات نقل الركاب (ride-hailing) تعمل في عدة دول. يتكون النظام من المكونات الرئيسية التالية:
|
||||||
|
|
||||||
|
| المكون | التقنية | الوصف |
|
||||||
|
|-------|---------|-------|
|
||||||
|
| **تطبيق الركاب (Intaleq)** | Flutter/Dart | تطبيق المستخدمين لطلب الرحلات |
|
||||||
|
| **تطبيق السائقين (intaleq_driver)** | Flutter/Dart | تطبيق السائقين (الكباتن) لاستقبال وتنفيذ الرحلات |
|
||||||
|
| **لوحة الإدارة (intaleq_admin)** | Flutter/Dart | لوحة تحكم للمشرفين لإدارة النظام |
|
||||||
|
| **تطبيق خدمة العملاء (service_intaleq)** | Flutter/Dart | تطبيق للموظفين للدعم وإدارة السائقين |
|
||||||
|
| **الخادم الرئيسي (intaleq_v1)** | PHP (بدون إطار عمل) | الباك إند الرئيسي REST API |
|
||||||
|
| **خادم المحفظة (Wallet Server)** | PHP (منفصل) | خدمة مالية مستقلة للمدفوعات |
|
||||||
|
| **خادم المواقع (Location Server)** | PHP (منفصل) | خدمة تتبع مواقع السائقين |
|
||||||
|
| **سيرفر السوكت (socket_intaleq)** | PHP WebSocket | خدمة الوقت الفعلي للتواصل المباشر |
|
||||||
|
| **خدمة OTP (flash-call-otp)** | Node.js | خدمة التحقق عبر المكالمات السريعة |
|
||||||
|
| **خدمة الخرائط (map-saas)** | خدمة منفصلة | خدمة التوجيه والبحث الجغرافي |
|
||||||
|
|
||||||
|
### 1.2 النطاق الجغرافي
|
||||||
|
|
||||||
|
النظام يعمل في عدة دول وأقاليم:
|
||||||
|
- **مصر** (القاهرة، الجيزة، الإسكندرية)
|
||||||
|
- **سوريا**
|
||||||
|
- **الأردن** (مسارات خاصة - routesjo)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. هيكل المشاريع والهندسة المعمارية
|
||||||
|
|
||||||
|
### 2.1 الرسم البياني للهندسة المعمارية
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ تطبيقات الواجهة الأمامية │
|
||||||
|
├─────────────┬──────────────┬──────────────┬──────────────────┤
|
||||||
|
│ Intaleq │ intaleq_ │ intaleq_ │ service_ │
|
||||||
|
│ (الركاب) │ driver │ admin │ intaleq │
|
||||||
|
│ Flutter │ Flutter │ Flutter │ Flutter │
|
||||||
|
└──────┬──────┴──────┬───────┴──────┬───────┴──────┬───────────┘
|
||||||
|
│ │ │ │
|
||||||
|
└─────────────┼──────────────┼──────────────┘
|
||||||
|
│ │
|
||||||
|
┌──────▼──────────────▼──────┐
|
||||||
|
│ API الرئيسية │
|
||||||
|
│ api.intaleq.xyz │
|
||||||
|
│ PHP REST (بدون إطار عمل) │
|
||||||
|
│ + JWT Authentication │
|
||||||
|
│ + Rate Limiting (Redis) │
|
||||||
|
└──────┬─────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────────┼─────────────────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────┐ ┌──────────────┐ ┌──────────────────┐
|
||||||
|
│ MySQL │ │ Redis │ │ خوادم فرعية │
|
||||||
|
│ intaleq │ │ (جلسات/تخزين │ │ - Wallet Server │
|
||||||
|
│ DB1 │ │ مؤقت/Cache) │ │ - Location Server│
|
||||||
|
└──────────┘ └──────────────┘ │ - Socket Server │
|
||||||
|
│ - Map SaaS │
|
||||||
|
└──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 هيكل مجلدات المشاريع
|
||||||
|
|
||||||
|
#### intaleq_admin (لوحة الإدارة)
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── main.dart # نقطة الدخول
|
||||||
|
├── routes.dart # تعريف المسارات
|
||||||
|
├── binding/
|
||||||
|
│ └── initial_binding.dart # ربط المتحكمات الأولية
|
||||||
|
├── constant/ # الثوابت
|
||||||
|
│ ├── api_key.dart # مفاتيح API سرية
|
||||||
|
│ ├── box_name.dart # مفاتيح التخزين المحلي
|
||||||
|
│ ├── colors.dart # ألوان التطبيق
|
||||||
|
│ ├── credential.dart # بيانات الاعتماد
|
||||||
|
│ ├── links.dart # روابط API (363 سطر - جميع نقاط النهاية)
|
||||||
|
│ ├── style.dart # أنماط التصميم
|
||||||
|
│ └── table_names.dart # أسماء الجداول
|
||||||
|
├── controller/ # طبقة المتحكمات
|
||||||
|
│ ├── auth/ # المصادقة
|
||||||
|
│ │ ├── login_controller.dart
|
||||||
|
│ │ └── otp_helper.dart
|
||||||
|
│ ├── admin/ # متحكمات لوحة الإدارة
|
||||||
|
│ │ ├── analytics_v2_controller.dart
|
||||||
|
│ │ ├── captain_admin_controller.dart
|
||||||
|
│ │ ├── complaint_controller.dart
|
||||||
|
│ │ ├── dashboard_controller.dart
|
||||||
|
│ │ ├── dashboard_v2_controller.dart
|
||||||
|
│ │ ├── driver_docs_controller.dart
|
||||||
|
│ │ ├── financial_v2_controller.dart
|
||||||
|
│ │ ├── get_all_invoice_controller.dart
|
||||||
|
│ │ ├── kazan_controller.dart
|
||||||
|
│ │ ├── passenger_admin_controller.dart
|
||||||
|
│ │ ├── promo_controller.dart
|
||||||
|
│ │ ├── quality_controller.dart
|
||||||
|
│ │ ├── register_captain_controller.dart
|
||||||
|
│ │ ├── ride_admin_controller.dart
|
||||||
|
│ │ ├── security_v2_controller.dart
|
||||||
|
│ │ ├── staff_controller.dart
|
||||||
|
│ │ ├── static_controller.dart
|
||||||
|
│ │ └── wallet_admin_controller.dart
|
||||||
|
│ ├── firebase/ # إشعارات Firebase
|
||||||
|
│ ├── functions/ # دوال مساعدة (CRUD، تشفير، AI)
|
||||||
|
│ └── bank_account/ # حسابات بنكية
|
||||||
|
├── models/ # نماذج البيانات
|
||||||
|
│ ├── db_sql.dart
|
||||||
|
│ └── model/
|
||||||
|
│ └── admin/ # نماذج المشرفين
|
||||||
|
├── views/ # واجهات المستخدم
|
||||||
|
│ ├── admin/ # شاشات لوحة الإدارة
|
||||||
|
│ │ ├── admin_home_page.dart # الصفحة الرئيسية (1123 سطر)
|
||||||
|
│ │ ├── dashboard_v2_widget.dart
|
||||||
|
│ │ ├── captain/ # إدارة السائقين
|
||||||
|
│ │ ├── complaints/ # الشكاوى
|
||||||
|
│ │ ├── drivers/ # تتبع السائقين
|
||||||
|
│ │ ├── employee/ # الموظفون
|
||||||
|
│ │ ├── enceypt/ # أدوات التشفير
|
||||||
|
│ │ ├── financial/ # المالية
|
||||||
|
│ │ ├── passenger/ # الركاب
|
||||||
|
│ │ ├── pricing/ # التسعير
|
||||||
|
│ │ ├── promo/ # العروض الترويجية
|
||||||
|
│ │ ├── quality/ # الجودة
|
||||||
|
│ │ ├── rides/ # الرحلات
|
||||||
|
│ │ ├── security/ # الأمان
|
||||||
|
│ │ ├── server/ # مراقبة السيرفر
|
||||||
|
│ │ ├── staff/ # الكوادر
|
||||||
|
│ │ ├── static/ # الإحصائيات
|
||||||
|
│ │ └── wallet/ # المحفظة
|
||||||
|
│ ├── auth/ # شاشات المصادقة
|
||||||
|
│ └── widgets/ # مكونات قابلة لإعادة الاستخدام
|
||||||
|
└── services/ # (فارغ - لا توجد خدمات منفصلة)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. تحليل الباك إند - الخادم الرئيسي
|
||||||
|
|
||||||
|
### 3.1 التقنيات المستخدمة
|
||||||
|
|
||||||
|
| التقنية | الوصف |
|
||||||
|
|---------|-------|
|
||||||
|
| **PHP** (بدون إطار عمل) | لغة السيرفر الأساسية - إصدار حديث مع `declare(strict_types=1)` |
|
||||||
|
| **MySQL** | قاعدة البيانات الرئيسية (intaleqDB1) |
|
||||||
|
| **Redis** | للتخزين المؤقت، إدارة الجلسات، Rate Limiting، والقوائم السوداء |
|
||||||
|
| **JWT** | للمصادقة والتوكنات |
|
||||||
|
| **PDO** | للاتصال الآمن بقاعدة البيانات |
|
||||||
|
| **Composer** | لإدارة المكتبات (Firebase JWT, Twilio, Stripe, PHPMailer) |
|
||||||
|
| **HMAC** | لتوقيع الطلبات المالية |
|
||||||
|
|
||||||
|
### 3.2 هيكل الباك إند
|
||||||
|
|
||||||
|
```
|
||||||
|
intaleq_v1/
|
||||||
|
├── connect.php # نقطة الدخول الموحدة (JWT مطلوب)
|
||||||
|
├── functions.php # دوال مساعدة (jsonSuccess, jsonError...)
|
||||||
|
├── login.php # تسجيل دخول API عام
|
||||||
|
├── loginAdmin.php # تسجيل دخول الأدمن (JWT)
|
||||||
|
├── core/
|
||||||
|
│ ├── bootstrap.php # البوابة الرئيسية: Redis, Encryption, Headers
|
||||||
|
│ ├── helpers.php # دوال البيئة والمساعدة
|
||||||
|
│ ├── Auth/
|
||||||
|
│ │ ├── JwtService.php # خدمة JWT كاملة
|
||||||
|
│ │ └── RateLimiter.php # تحديد معدل الطلبات
|
||||||
|
│ ├── Database/
|
||||||
|
│ │ └── Database.php # اتصال قاعدة البيانات (Singleton)
|
||||||
|
│ ├── Security/
|
||||||
|
│ │ └── EncryptionHelper.php # مساعد التشفير
|
||||||
|
│ └── Services/ # خدمات (فارغ حالياً)
|
||||||
|
├── Admin/ # نقاط نهاية لوحة الإدارة
|
||||||
|
│ ├── dashbord.php # لوحة المعلومات الرئيسية
|
||||||
|
│ ├── v2/ # خدمات الجيل الثاني
|
||||||
|
│ │ ├── realtime_dashboard.php
|
||||||
|
│ │ ├── smart_alerts.php
|
||||||
|
│ │ ├── analytics/ # التحليلات (نمو، إيرادات، ترتيب)
|
||||||
|
│ │ ├── financial/ # مالية (تسويات، إحصائيات)
|
||||||
|
│ │ ├── quality/ # جودة (قائمة سوداء، تقييم)
|
||||||
|
│ │ └── security/ # أمان (سجل التدقيق)
|
||||||
|
│ ├── adminUser/ # إدارة المشرفين
|
||||||
|
│ ├── driver/ # إدارة السائقين
|
||||||
|
│ ├── passenger/ # إدارة الركاب
|
||||||
|
│ ├── rides/ # إدارة الرحلات
|
||||||
|
│ ├── Staff/ # إدارة الكوادر
|
||||||
|
│ └── employee/ # الموظفون
|
||||||
|
├── auth/ # المصادقة
|
||||||
|
│ ├── captin/ # تسجيل/دخول السائقين
|
||||||
|
│ ├── sms/ # إرسال الرسائل النصية
|
||||||
|
│ ├── google_auth/ # مصادقة جوجل
|
||||||
|
│ ├── syria/ # مصادقة سوريا
|
||||||
|
│ └── passengerOTP/ # OTP الركاب
|
||||||
|
├── ride/ # عمليات الرحلات
|
||||||
|
│ ├── rides/ # إدارة الرحلات
|
||||||
|
│ ├── cancelRide/ # إلغاء الرحلات
|
||||||
|
│ ├── driver_order/ # طلبات السائقين
|
||||||
|
│ ├── kazan/ # التسعير
|
||||||
|
│ ├── promo/ # العروض الترويجية
|
||||||
|
│ ├── rate/ # التقييمات
|
||||||
|
│ ├── location/ # المواقع
|
||||||
|
│ ├── firebase/ # إشعارات Firebase
|
||||||
|
│ ├── driverWallet/ # محفظة السائقين
|
||||||
|
│ ├── passengerWallet/ # محفظة الركاب
|
||||||
|
│ ├── payment/ # المدفوعات
|
||||||
|
│ ├── notificationCaptain/ # إشعارات السائقين
|
||||||
|
│ ├── notificationPassenger/# إشعارات الركاب
|
||||||
|
│ ├── feedBack/ # التقييمات
|
||||||
|
│ ├── chat/ # المحادثة
|
||||||
|
│ ├── gamification/ # نظام التحفيز
|
||||||
|
│ └── invitor/ # نظام الدعوات
|
||||||
|
├── serviceapp/ # خدمة العملاء
|
||||||
|
│ ├── getComplaintAllData.php
|
||||||
|
│ ├── editCarPlate.php
|
||||||
|
│ ├── addNotesDriver.php
|
||||||
|
│ └── ... (20+ نقطة نهاية)
|
||||||
|
├── migration/ # الترحيل والتحديثات
|
||||||
|
├── EgyptDocuments/ # وثائق مصر
|
||||||
|
├── email/ # البريد الإلكتروني
|
||||||
|
├── webhook_sms/ # Webhook للرسائل
|
||||||
|
└── schema_*.sql # مخططات قواعد البيانات
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 نظام الأمان في الباك إند
|
||||||
|
|
||||||
|
1. **JWT Authentication**: جميع الطلبات تتطلب توكن JWT صالح
|
||||||
|
2. **Rate Limiting**: عبر Redis لمنع الهجمات
|
||||||
|
3. **تشفير البيانات**: `EncryptionHelper` مع مفتاح 32 بايت
|
||||||
|
4. **HMAC للمعاملات المالية**: توقيع إضافي للخادم المالي
|
||||||
|
5. **CORS Headers**: مقيدة وآمنة
|
||||||
|
6. **Device Fingerprint**: تتبع الأجهزة عبر `X-Device-FP`
|
||||||
|
|
||||||
|
### 3.4 نقاط القوة في الباك إند
|
||||||
|
|
||||||
|
- ✅ بنية تحتية قوية مع Redis و JWT و Rate Limiting
|
||||||
|
- ✅ فصل الخدمات (Wallet, Location, Map SaaS مستقلة)
|
||||||
|
- ✅ نظام تحقق متعدد (OTP, Google, Flash Call)
|
||||||
|
- ✅ دعم متعدد الدول (مصر، سوريا، الأردن)
|
||||||
|
- ✅ نظام ترحيل (Migration) لتحديث قاعدة البيانات
|
||||||
|
|
||||||
|
### 3.5 نقاط الضعف في الباك إند
|
||||||
|
|
||||||
|
- ❌ **عدم استخدام إطار عمل**: PHP خام بدون Laravel/Symfony مما يصعب الصيانة
|
||||||
|
- ❌ **SQL مباشر**: استعلامات SQL مكتوبة يدوياً بدون ORM (عرضة للـ SQL Injection في بعض الأماكن)
|
||||||
|
- ❌ **غياب التوثيق**: لا يوجد API documentation (Swagger/OpenAPI)
|
||||||
|
- ❌ **غياب الاختبارات**: لا توجد Unit Tests أو Integration Tests
|
||||||
|
- ❌ **ملف connect.php المركزي**: نقطة فشل واحدة (Single Point of Failure)
|
||||||
|
- ❌ **كود مكرر**: دوال jsonSuccess/jsonError مكررة في أغلب الملفات
|
||||||
|
- ❌ **غياب logging منظم**: استخدام `error_log` فقط بدون مستويات
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. تحليل لوحة الإدارة
|
||||||
|
|
||||||
|
### 4.1 المميزات الحالية
|
||||||
|
|
||||||
|
لوحة الإدارة توفر الوظائف التالية مقسمة حسب الفئات:
|
||||||
|
|
||||||
|
#### 🧑🤝🧑 المستخدمين
|
||||||
|
- **الركاب**: عرض وإدارة وحذف وحظر
|
||||||
|
- **السائقون**: عرض وإدارة وتفعيل وحذف
|
||||||
|
- **المراقب**: تتبع السائقين مباشرة على الخريطة
|
||||||
|
|
||||||
|
#### ⚙️ إدارة النظام
|
||||||
|
- **أكواد الخصم**: إنشاء وتعديل وحذف (Promo Management)
|
||||||
|
- **تعديل الأسعار**: تعديل نسب التسعير (Kazan Editor)
|
||||||
|
- **الشكاوى**: عرض وإدارة الشكاوى
|
||||||
|
- **مراجعة الوثائق**: فحص وثائق السائقين
|
||||||
|
|
||||||
|
#### 📊 العمليات
|
||||||
|
- **الرحلات**: عرض وإدارة جميع الرحلات
|
||||||
|
- **مراقبة الرحلات**: متابعة الرحلات الجارية
|
||||||
|
- **الإحصائيات**: إحصائيات عامة
|
||||||
|
- **التحليلات المتقدمة**: تحليلات V2
|
||||||
|
|
||||||
|
#### 🛡️ الجودة والدعم
|
||||||
|
- **القائمة السوداء**: حظر سائقين/ركاب
|
||||||
|
|
||||||
|
#### 💰 المالية والإدارة
|
||||||
|
- **الإدارة المالية V2**: لوحة مالية متقدمة
|
||||||
|
- **المحفظة**: إدارة المحافظ
|
||||||
|
- **هدية 300**: هدايا تحفيزية للسائقين
|
||||||
|
- **الفواتير**: إدارة الفواتير
|
||||||
|
- **الموظفون**: إدارة الموظفين
|
||||||
|
- **موافقة المشرفين**: قبول/رفض مشرفين جدد
|
||||||
|
|
||||||
|
#### 🔧 النظام والتواصل
|
||||||
|
- **سجل العمليات**: سجل تدقيق أمني
|
||||||
|
- **واتساب جماعي**: رسائل جماعية للسائقين
|
||||||
|
- **إشعار سائقين/ركاب**: إشعارات Firebase
|
||||||
|
- **تسجيل سائق**: تسجيل سائقين جدد
|
||||||
|
- **تحديث التطبيق**: متابعة إصدارات التطبيق
|
||||||
|
- **مراقب السيرفر**: مراقبة حالة السيرفر
|
||||||
|
- **سجل الأخطاء**: عرض أخطاء النظام
|
||||||
|
- **أدوات التشفير**: تشفير بصمات الأجهزة
|
||||||
|
- **إدارة الكوادر**: إضافة مديرين وخدمة عملاء (للسوبر أدمن فقط)
|
||||||
|
|
||||||
|
### 4.2 نظام الصلاحيات
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// التحقق من الصلاحيات في admin_home_page.dart
|
||||||
|
isSuperAdmin = (role == 'super_admin') ||
|
||||||
|
(myPhone == '201023248456' || // أرقام محددة صلاحية مطلقة
|
||||||
|
myPhone == '963992952235' ||
|
||||||
|
myPhone == '963942542053');
|
||||||
|
```
|
||||||
|
|
||||||
|
**مستويات الصلاحيات:**
|
||||||
|
1. **super_admin**: صلاحية كاملة - رؤية المحفظة، إضافة كوادر، مراقبة الرحلات
|
||||||
|
2. **admin**: صلاحية محدودة - إدارة عامة بدون صلاحيات مالية حساسة
|
||||||
|
3. **service**: خدمة عملاء (غير مفعلة في كل الواجهات)
|
||||||
|
|
||||||
|
### 4.3 نقاط القوة في لوحة الإدارة
|
||||||
|
|
||||||
|
- ✅ **تصميم عصري وجميل**: استخدام ألوان داكنة مع تأثيرات بصرية جذابة
|
||||||
|
- ✅ **لوحة معلومات تفاعلية**: Dashboard V2 مع تحديث تلقائي كل دقيقتين
|
||||||
|
- ✅ **بحث سريع**: شريط بحث لتصفية الخدمات
|
||||||
|
- ✅ **تصنيف منظم**: تجميع الوظائف في فئات منطقية
|
||||||
|
- ✅ **دعم اللغة العربية**: الواجهة بالكامل بالعربية
|
||||||
|
- ✅ **تحديث تلقائي**: RefreshIndicator للسحب للتحديث
|
||||||
|
|
||||||
|
### 4.4 نقاط الضعف في لوحة الإدارة
|
||||||
|
|
||||||
|
- ❌ **ملف routes.dart غير مكتمل**: 7 مسارات فقط معرفة من أصل 30+ شاشة (الباقي عبر `Get.to()`)
|
||||||
|
- ❌ **كود كبير جداً**: `admin_home_page.dart` يحتوي 1123 سطر (يجب تقسيمه)
|
||||||
|
- ❌ **تكرار الكود**: نمط CRUD متكرر في أغلب المتحكمات
|
||||||
|
- ❌ **معالجة أخطاء غير متسقة**: بعض المتحكمات تستخدم try-catch والبعض لا
|
||||||
|
- ❌ **لا يوجد pagination**: جميع البيانات تُجلب دفعة واحدة
|
||||||
|
- ❌ **غياب التخزين المؤقت**: لا يوجد caching للبيانات محلياً
|
||||||
|
- ❌ **التحقق من الصلاحيات عبر أرقام هواتف ثابتة**: ممارسة غير آمنة
|
||||||
|
- ❌ **لا يوجد تسجيل خروج صريح**: إزالة التوكن فقط دون إلغاء في السيرفر
|
||||||
|
- ❌ **استخدام GetX لإدارة الحالة فقط**: بدون استخدام ميزات التنقل القوية في GetX
|
||||||
|
- ❌ **غياب WebSocket**: لا يوجد اتصال مباشر للتحديثات الفورية (باستثناء V2 timer)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. تحليل تطبيق خدمة العملاء
|
||||||
|
|
||||||
|
### 5.1 الوظائف الرئيسية
|
||||||
|
|
||||||
|
تطبيق `service_intaleq` هو أداة لموظفي خدمة العملاء للقيام بـ:
|
||||||
|
- تسجيل الدخول للموظفين
|
||||||
|
- استقبال إشعارات Firebase
|
||||||
|
- الوصول إلى وظائف serviceapp في الباك إند
|
||||||
|
|
||||||
|
### 5.2 نقاط القوة
|
||||||
|
- ✅ دعم اللغات (العربية والإنجليزية)
|
||||||
|
- ✅ تكامل مع Firebase للإشعارات
|
||||||
|
|
||||||
|
### 5.3 نقاط الضعف
|
||||||
|
- ❌ تطبيق بسيط جداً - يبدو غير مكتمل
|
||||||
|
- ❌ لا توجد شاشات واضحة للموظفين
|
||||||
|
- ❌ تكرار مع وظائف لوحة الإدارة
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. قاعدة البيانات والموديلات
|
||||||
|
|
||||||
|
### 6.1 الجداول الرئيسية (44+ جدول)
|
||||||
|
|
||||||
|
#### جداول المستخدمين
|
||||||
|
| الجدول | الوصف | عدد الأعمدة |
|
||||||
|
|-------|-------|------------|
|
||||||
|
| `passengers` | الركاب | ~20 عمود |
|
||||||
|
| `driver` | السائقين | ~25 عمود |
|
||||||
|
| `adminUser` | المشرفين | 4 أعمدة |
|
||||||
|
| `employee` | الموظفين | 6 أعمدة |
|
||||||
|
|
||||||
|
#### جداول الرحلات
|
||||||
|
| الجدول | الوصف |
|
||||||
|
|-------|-------|
|
||||||
|
| `ride` | الرحلات الرئيسية |
|
||||||
|
| `canecl` | إلغاءات الرحلات |
|
||||||
|
| `driver_orders` | طلبات السائقين |
|
||||||
|
| `driver_behavior` | سلوك السائقين |
|
||||||
|
| `driver_ride_scam` | احتيال الرحلات |
|
||||||
|
|
||||||
|
#### جداول المركبات
|
||||||
|
| الجدول | الوصف |
|
||||||
|
|-------|-------|
|
||||||
|
| `CarRegistration` | تسجيل المركبات |
|
||||||
|
| `captains_car` | سيارات السائقين |
|
||||||
|
| `car_locations` | مواقع السيارات (Spatial Index) |
|
||||||
|
| `car_tracks` | تتبع مسار السيارات |
|
||||||
|
|
||||||
|
#### جداول مالية
|
||||||
|
| الجدول | الوصف |
|
||||||
|
|-------|-------|
|
||||||
|
| `driverWallet` | محفظة السائقين |
|
||||||
|
| `driver_gifts` | هدايا السائقين |
|
||||||
|
| `invoice_records` | سجلات الفواتير |
|
||||||
|
| `invoicesAdmin` | فواتير المشرفين |
|
||||||
|
| `kazan` | التسعير |
|
||||||
|
|
||||||
|
#### جداول النظام
|
||||||
|
| الجدول | الوصف |
|
||||||
|
|-------|-------|
|
||||||
|
| `complaint` | الشكاوى |
|
||||||
|
| `error` | سجل الأخطاء (115,338+ سجل) |
|
||||||
|
| `notifications` | إشعارات الركاب |
|
||||||
|
| `notificationCaptain` | إشعارات السائقين |
|
||||||
|
| `login_attempts` | محاولات الدخول |
|
||||||
|
| `api_keys` | مفاتيح API |
|
||||||
|
|
||||||
|
### 6.2 نقاط القوة
|
||||||
|
- ✅ **تصميم جغرافي مكاني**: استخدام `POINT` و `SPATIAL INDEX` في جدول `car_locations`
|
||||||
|
- ✅ **تتبع كامل**: جداول لتتبع السلوك، المواقع، والمسار
|
||||||
|
- ✅ **أمان**: جداول لتسجيل المحاولات والأخطاء
|
||||||
|
|
||||||
|
### 6.3 نقاط الضعف
|
||||||
|
- ❌ **عدم استخدام FK (Foreign Keys)**: العلاقات غير مقيدة
|
||||||
|
- ❌ **عدم تناسق التسمية**: camelCase مع snake_case في نفس الجدول
|
||||||
|
- ❌ **عدم وجود indexing كافٍ**: بعض الأعمدة المستخدمة بكثرة بدون INDEX
|
||||||
|
- ❌ **استخدام جداول قديمة**: مثل `captains_car` و `CarRegistration` المكررين
|
||||||
|
- ❌ **تخزين بيانات حساسة**: `password` غير مشفرة بشكل كافٍ
|
||||||
|
- ❌ **استخدام VARCHAR(6) لـ amount**: يجب أن يكون DECIMAL للمبالغ المالية
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. نظام المصادقة والصلاحيات
|
||||||
|
|
||||||
|
### 7.1 تدفق المصادقة
|
||||||
|
|
||||||
|
```
|
||||||
|
1. المستخدم ← رقم الهاتف + كلمة المرور
|
||||||
|
2. الباك إند ← التحقق + JWT (مع role)
|
||||||
|
3. التطبيق ← تخزين JWT مشفر في GetStorage
|
||||||
|
4. كل طلب ← إرسال JWT في Header Authorization
|
||||||
|
5. الباك إند ← فك JWT + استخراج role + التحقق
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 نظام التوكن
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// طريقة الحصول على JWT في CRUD
|
||||||
|
getJWT() async {
|
||||||
|
var payload = {
|
||||||
|
'id': 'admin',
|
||||||
|
'password': AK.passnpassenger,
|
||||||
|
'aud': '${AK.allowed}$dev',
|
||||||
|
};
|
||||||
|
// تشفير التوكن 3 مرات:
|
||||||
|
await box.write(BoxName.jwt, X.c(X.c(X.c(jwt, cn), cC), cs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// فك التشفير عند الاستخدام:
|
||||||
|
token = r(rawJwt.toString()).split(AppInformation.addd)[0];
|
||||||
|
```
|
||||||
|
|
||||||
|
**طبقات الأمان:**
|
||||||
|
1. **JWT** - توقيع رمزي
|
||||||
|
2. **تشفير 3 طبقات** - `X.c()` ثلاث مرات متتالية
|
||||||
|
3. **HMAC** - للمعاملات المالية
|
||||||
|
4. **Device Fingerprint** - `X-Device-FP` header
|
||||||
|
|
||||||
|
### 7.3 نقاط القوة
|
||||||
|
- ✅ تشفير متعدد الطبقات للتوكنات
|
||||||
|
- ✅ نظام صلاحيات قائم على الأدوار (role-based)
|
||||||
|
- ✅ فصل مالي مع HMAC
|
||||||
|
|
||||||
|
### 7.4 نقاط الضعف
|
||||||
|
- ❌ **أرقام هواتف ثابتة للسوبر أدمن**: ممارسة غير آمنة
|
||||||
|
- ❌ **عدم وجود انتهاء صلاحية فعال**: إعادة تلقائية دون تدخل المستخدم
|
||||||
|
- ❌ **تخزين التوكن محلياً**: بدون Secure Enclave
|
||||||
|
- ❌ **كلمة مرور ثابتة في الكود**: `AK.passnpassenger`
|
||||||
|
- ❌ **لا يوجد 2FA**: للمشرفين
|
||||||
|
- ❌ **لا يوجد audit log للتغييرات الحساسة**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. تحليل التوافقية بين الباك إند والأدمن
|
||||||
|
|
||||||
|
### 8.1 مصفوفة التوافق
|
||||||
|
|
||||||
|
| الوظيفة | الباك إند | الأدمن | الحالة |
|
||||||
|
|--------|----------|--------|--------|
|
||||||
|
| لوحة المعلومات | `Admin/dashbord.php` | `DashboardController` | ✅ متوافق |
|
||||||
|
| لوحة V2 | `Admin/v2/realtime_dashboard.php` | `DashboardV2Controller` | ✅ متوافق |
|
||||||
|
| قائمة السائقين | `Admin/AdminCaptain/get.php` | `captain_admin_controller.dart` | ✅ متوافق |
|
||||||
|
| تفاصيل السائق | `auth/syria/driver/driver_details.php` | `captain_details.dart` | ✅ متوافق |
|
||||||
|
| الركاب | `Admin/getPassengerDetails.php` | `passenger_admin_controller.dart` | ✅ متوافق |
|
||||||
|
| أكواد الخصم | `ride/promo/` | `promo_controller.dart` | ✅ متوافق |
|
||||||
|
| التسعير | `ride/kazan/` | `kazan_controller.dart` | ✅ متوافق |
|
||||||
|
| الشكاوى | `serviceapp/getComplaintAllData.php` | `complaint_controller.dart` | ✅ متوافق |
|
||||||
|
| المالية V2 | `v2/financial/` | `financial_v2_controller.dart` | ✅ متوافق |
|
||||||
|
| سجل التدقيق | `v2/security/audit_logs.php` | `security_v2_controller.dart` | ✅ متوافق |
|
||||||
|
| الإحصائيات | `Admin/getRidesPerMonth.php` | `static_controller.dart` | ✅ متوافق |
|
||||||
|
| المحادثة | `ride/chat/` | ❌ غير موجود | ❌ غير متوافق |
|
||||||
|
| نظام التحفيز | `ride/gamification/` | ❌ غير موجود | ❌ غير متوافق |
|
||||||
|
| تتبع حي | WebSocket | ❌ غير موجود | ❌ غير متوافق |
|
||||||
|
|
||||||
|
### 8.2 مشاكل التوافقية
|
||||||
|
|
||||||
|
1. **ملف routes.dart غير مكتمل**: 7 مسارات فقط من أصل 30+ وظيفة تستخدم `Get.toNamed()`
|
||||||
|
2. **استخدام `Get.to()` المباشر**: بدون تعريفها في routes، مما يصعب تتبع التنقل
|
||||||
|
3. **اختلاف أسماء الحقول**: بعض الـ payload تختلف تسميتها بين الأدمن والباك إند
|
||||||
|
4. **غياب معالجة Versioning**: لا توجد آلية لتحديد إصدار API
|
||||||
|
5. **نقاط نهاية غير مستخدمة**: بعض endpoints في الباك إند لا تقابلها شاشات في الأدمن
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. الإضافات والتحسينات المقترحة
|
||||||
|
|
||||||
|
### 9.1 تحسينات أمنية (أولوية عالية) 🔴
|
||||||
|
|
||||||
|
1. **إضافة 2FA للمشرفين**
|
||||||
|
- إضافة مصادقة ثنائية عبر Google Authenticator أو OTP
|
||||||
|
- حماية الوصول للوحة الإدارة
|
||||||
|
|
||||||
|
2. **تحسين نظام الصلاحيات**
|
||||||
|
- استبدال أرقام الهواتف الثابتة بنظام صلاحيات ديناميكي
|
||||||
|
- إضافة صلاحيات دقيقة (Granular Permissions) بدلاً من admin/super_admin فقط
|
||||||
|
|
||||||
|
3. **تسجيل الخروج من جميع الأجهزة**
|
||||||
|
- إضافة `token_blacklist` في Redis
|
||||||
|
- إمكانية إنهاء جلسات محددة
|
||||||
|
|
||||||
|
4. **تشفير البيانات الحساسة**
|
||||||
|
- نقل `api_key.dart` إلى متغيرات بيئة آمنة
|
||||||
|
- استخدام Secure Enclave لتخزين التوكنات
|
||||||
|
|
||||||
|
### 9.2 تحسينات معمارية (أولوية متوسطة) 🟡
|
||||||
|
|
||||||
|
5. **إعادة هيكلة routes.dart**
|
||||||
|
- تعريف جميع المسارات (~30 مسار) في ملف routes
|
||||||
|
- استخدام `GetPage` مع `binding` و `middleware`
|
||||||
|
|
||||||
|
6. **تقسيم admin_home_page.dart**
|
||||||
|
- فصل إلى مكونات مستقلة (StatelessWidget)
|
||||||
|
- نقل كل قسم إلى ملف منفصل
|
||||||
|
|
||||||
|
7. **طبقة خدمة موحدة (Service Layer)**
|
||||||
|
- إنشاء BaseRepository/BaseService
|
||||||
|
- توحيد معالجة الـ CRUD
|
||||||
|
- إضافة retry policy و offline support
|
||||||
|
|
||||||
|
8. **استخدام Riverpod/Bloc بدلاً من GetX**
|
||||||
|
- GetX يعمل ولكن ليس الخيار الأمثل للتطبيقات الكبيرة
|
||||||
|
- تحسين فصل المسؤوليات (Separation of Concerns)
|
||||||
|
|
||||||
|
### 9.3 تحسينات وظيفية (أولوية متوسطة) 🟡
|
||||||
|
|
||||||
|
9. **لوحة معلومات قابلة للتخصيص**
|
||||||
|
- إضافة Widgets قابلة للسحب والإفلات
|
||||||
|
- حفظ تفضيلات المستخدم
|
||||||
|
|
||||||
|
10. **تقارير PDF مصدرة**
|
||||||
|
- تصدير الإحصائيات كـ PDF
|
||||||
|
- جداولة التقارير الدورية
|
||||||
|
|
||||||
|
11. **نظام تنبيهات ذكي**
|
||||||
|
- تنبيهات تلقائية عند تجاوز الحدود (عدد شكاوى، إلغاءات)
|
||||||
|
- Machine Learning للكشف عن السلوك الاحتيالي
|
||||||
|
|
||||||
|
12. **محادثة مباشرة مع السائقين والركاب**
|
||||||
|
- دمج chat داخل لوحة الإدارة
|
||||||
|
- استخدام WebSocket للتواصل الفوري
|
||||||
|
|
||||||
|
### 9.4 تحسينات تقنية (أولوية منخفضة) 🟢
|
||||||
|
|
||||||
|
13. **Pagination و Infinite Scroll**
|
||||||
|
- لجميع القوائم (السائقين، الركاب، الرحلات)
|
||||||
|
|
||||||
|
14. **WebSocket للوحة الإدارة**
|
||||||
|
- اتصال مباشر مع socket_intaleq
|
||||||
|
- تحديثات فورية بدون polling
|
||||||
|
|
||||||
|
15. **دعم الوضع المظلم/الفاتح**
|
||||||
|
- إضافة theme toggle
|
||||||
|
- دعم system theme
|
||||||
|
|
||||||
|
16. **اختبارات واجهة المستخدم**
|
||||||
|
- Widget Tests و Integration Tests
|
||||||
|
|
||||||
|
### 9.5 إضافات جديدة مقترحة (أولوية متنوعة) 💡
|
||||||
|
|
||||||
|
17. **لوحة تحليلات الأعمال (BI Dashboard)**
|
||||||
|
- رسوم بيانية تفاعلية
|
||||||
|
- مقارنة الأداء بين الفترات
|
||||||
|
- توقع الإيرادات المستقبلية
|
||||||
|
|
||||||
|
18. **نظام إدارة المناطق الجغرافية**
|
||||||
|
- رسم المناطق النشطة على الخريطة
|
||||||
|
- تحليل الكثافة والتغطية
|
||||||
|
- إدارة المناطق المحظورة
|
||||||
|
|
||||||
|
19. **نظام Gamification Dashboard**
|
||||||
|
- لوحة كاملة لنظام التحفيز (موجود API بدون UI)
|
||||||
|
- عرض تصنيفات السائقين
|
||||||
|
- إدارة المكافآت والتحديات
|
||||||
|
|
||||||
|
20. **نظام إدارة الحملات التسويقية**
|
||||||
|
- إنشاء حملات إشعارات مستهدفة
|
||||||
|
- A/B Testing للإشعارات
|
||||||
|
- قياس فعالية الحملات
|
||||||
|
|
||||||
|
21. **تكامل مع منصات خارجية**
|
||||||
|
- تصدير البيانات إلى Google Sheets
|
||||||
|
- Slack/Telegram notifications للإشعارات المهمة
|
||||||
|
- Webhook للمطورين الخارجيين
|
||||||
|
|
||||||
|
22. **نظام توثيق API تلقائي**
|
||||||
|
- Swagger/OpenAPI documentation
|
||||||
|
- Sandbox environment للاختبار
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. ملخص وتوصيات
|
||||||
|
|
||||||
|
### 10.1 ملخص الحالة الراهنة
|
||||||
|
|
||||||
|
نظام إنتلق هو نظام متكامل وقوي لتطبيقات نقل الركاب، مع:
|
||||||
|
- **بنية تحتية متقدمة**: Redis، JWT، HMAC، Rate Limiting
|
||||||
|
- **مجموعة واسعة من الميزات**: إدارة ركاب، سائقين، رحلات، مالية، شكاوى، إشعارات
|
||||||
|
- **دعم متعدد الدول**: مصر، سوريا، الأردن مع إمكانية التوسع
|
||||||
|
- **تصميم عصري**: واجهة مستخدم جذابة باللغة العربية
|
||||||
|
|
||||||
|
**نقاط القوة الرئيسية:**
|
||||||
|
1. نظام مصادقة آمن مع JWT و HMAC متعدد الطبقات
|
||||||
|
2. فصل الخدمات المالية والجغرافية في خوادم مستقلة
|
||||||
|
3. دعم التتبع الجغرافي المكاني (Spatial Index)
|
||||||
|
4. نظام V2 للوحة المعلومات بتحديث تلقائي
|
||||||
|
5. واجهة إدارة شاملة تغطي معظم جوانب النظام
|
||||||
|
|
||||||
|
**نقاط الضعف الرئيسية:**
|
||||||
|
1. عدم استخدام إطار عمل PHP (صعوبة في الصيانة والتوسع)
|
||||||
|
2. كود كبير في ملفات مفردة (1123 سطر في admin_home_page.dart)
|
||||||
|
3. نظام صلاحيات بدائي مع أرقام هواتف ثابتة
|
||||||
|
4. نقص التوثيق والاختبارات
|
||||||
|
5. طرق تنقل غير متسقة في التطبيق (مزيج بين Get.toNamed و Get.to)
|
||||||
|
|
||||||
|
### 10.2 خريطة الطريق المقترحة
|
||||||
|
|
||||||
|
```
|
||||||
|
المرحلة الأولى (شهر 1-2) - تحسينات أمنية وحرجة:
|
||||||
|
├── 2FA للمشرفين
|
||||||
|
├── تحسين نظام الصلاحيات
|
||||||
|
├── نقل المفاتيح السرية إلى بيئة آمنة
|
||||||
|
└── إصلاح الثغرات الأمنية
|
||||||
|
|
||||||
|
المرحلة الثانية (شهر 3-4) - تحسينات معمارية:
|
||||||
|
├── إعادة هيكلة routes
|
||||||
|
├── تقسيم الملفات الكبيرة
|
||||||
|
├── إضافة طبقة خدمة موحدة
|
||||||
|
└── Pagination لكل القوائم
|
||||||
|
|
||||||
|
المرحلة الثالثة (شهر 5-6) - إضافات جديدة:
|
||||||
|
├── لوحة BI Dashboard
|
||||||
|
├── WebSocket Integration
|
||||||
|
├── تقارير PDF
|
||||||
|
└── نظام إدارة المناطق
|
||||||
|
|
||||||
|
المرحلة الرابعة (شهر 7-8) - تحسينات متقدمة:
|
||||||
|
├── API Documentation (Swagger)
|
||||||
|
├── اختبارات آلية
|
||||||
|
├── Offline Support
|
||||||
|
└── تكامل مع منصات خارجية
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.3 توصيات فورية
|
||||||
|
|
||||||
|
1. **🚨 عاجل**: نقل `api_key.dart` إلى متغيرات بيئة (`.env`) وليس في الكود
|
||||||
|
2. **🚨 عاجل**: إزالة أرقام الهواتف الثابتة من كود التحقق من الصلاحيات
|
||||||
|
3. **⚠️ مهم**: إكمال ملف `routes.dart` بجميع المسارات
|
||||||
|
4. **⚠️ مهم**: إضافة `INDEX` للأعمدة المستخدمة بكثرة في قاعدة البيانات
|
||||||
|
5. **📝 موصى به**: إنشاء `README.md` شامل لكل مشروع
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**تم إعداد هذا التقرير بواسطة التحليل الآلي للنظام**
|
||||||
|
|
||||||
|
*تاريخ الإعداد: 6 يناير 2026*
|
||||||
|
|
||||||
|
*الإصدار: 1.0*
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
38
pubspec.lock
38
pubspec.lock
@@ -289,7 +289,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.5+2"
|
version: "0.3.5+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
@@ -320,6 +320,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.6"
|
version: "3.1.6"
|
||||||
|
dart_webrtc:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: dart_webrtc
|
||||||
|
sha256: f6d615bddea5e458ce180a914f3055c234ffb52fb7397a51b3491e76d6d7edb2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.8.1"
|
||||||
dbus:
|
dbus:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -742,6 +750,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_webrtc:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_webrtc
|
||||||
|
sha256: c7b0a67ca2c878575fc5c146d801cd874f58f5f1ef5fa6e8eb0c93d413beb948
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
flutter_widget_from_html:
|
flutter_widget_from_html:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1324,6 +1340,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.1"
|
version: "6.0.1"
|
||||||
|
logger:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: logger
|
||||||
|
sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.7.0"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2224,6 +2248,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "3.0.3"
|
||||||
|
webrtc_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: webrtc_interface
|
||||||
|
sha256: c6f100eac5057d9a817a60473126f9828c796d42884d498af4f339c97b21014f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.5.1"
|
||||||
webview_flutter:
|
webview_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -2233,7 +2265,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.9.0"
|
version: "4.9.0"
|
||||||
webview_flutter_android:
|
webview_flutter_android:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_android
|
name: webview_flutter_android
|
||||||
sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558"
|
sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558"
|
||||||
@@ -2249,7 +2281,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.14.0"
|
version: "2.14.0"
|
||||||
webview_flutter_wkwebview:
|
webview_flutter_wkwebview:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: webview_flutter_wkwebview
|
name: webview_flutter_wkwebview
|
||||||
sha256: "108bd85d0ff20bff1e8b52a040f5c19b6b9fc4a78fdf3160534ff5a11a82e267"
|
sha256: "108bd85d0ff20bff1e8b52a040f5c19b6b9fc4a78fdf3160534ff5a11a82e267"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
cupertino_icons: ^1.0.8
|
cupertino_icons: ^1.0.8
|
||||||
|
flutter_webrtc: ^1.4.1
|
||||||
secure_string_operations:
|
secure_string_operations:
|
||||||
path: ./secure_string_operations
|
path: ./secure_string_operations
|
||||||
firebase_messaging: ^16.1.1
|
firebase_messaging: ^16.1.1
|
||||||
@@ -19,6 +20,7 @@ dependencies:
|
|||||||
path: ^1.9.1
|
path: ^1.9.1
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
http: ^1.2.2
|
http: ^1.2.2
|
||||||
|
crypto: ^3.0.3
|
||||||
get:
|
get:
|
||||||
path: ./packages/get
|
path: ./packages/get
|
||||||
get_storage:
|
get_storage:
|
||||||
@@ -56,6 +58,8 @@ dependencies:
|
|||||||
record: ^6.2.0
|
record: ^6.2.0
|
||||||
dio: ^5.9.1
|
dio: ^5.9.1
|
||||||
webview_flutter: ^4.9.0
|
webview_flutter: ^4.9.0
|
||||||
|
webview_flutter_android: ^3.16.2
|
||||||
|
webview_flutter_wkwebview: ^3.14.0
|
||||||
just_audio: ^0.10.5
|
just_audio: ^0.10.5
|
||||||
# share: ^2.0.4
|
# share: ^2.0.4
|
||||||
google_sign_in: ^7.2.0
|
google_sign_in: ^7.2.0
|
||||||
|
|||||||
201
scratch/benchmark_route.py
Normal file
201
scratch/benchmark_route.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
|
# ==================== CONFIGURATION ====================
|
||||||
|
API_KEY = "" # Kept your actual API key
|
||||||
|
BASE_URL = "https://map-saas.intaleqapp.com/api/maps/route"
|
||||||
|
CONCURRENCY = 1000 # Number of concurrent threads
|
||||||
|
TOTAL_REQUESTS = 10000 # Total number of requests to send
|
||||||
|
TIMEOUT_SECONDS = 10 # Request timeout
|
||||||
|
|
||||||
|
# Bounding boxes for heavily populated regions inside Jordan and Syria (excluding deserts and Egypt since it is not in the map database)
|
||||||
|
REGIONS = {
|
||||||
|
"Amman (Jordan)": {
|
||||||
|
"lat_min": 31.85, "lat_max": 32.15,
|
||||||
|
"lng_min": 35.80, "lng_max": 36.00
|
||||||
|
},
|
||||||
|
"Irbid (Jordan)": {
|
||||||
|
"lat_min": 32.45, "lat_max": 32.60,
|
||||||
|
"lng_min": 35.80, "lng_max": 35.95
|
||||||
|
},
|
||||||
|
"Damascus (Syria)": {
|
||||||
|
"lat_min": 33.45, "lat_max": 33.55,
|
||||||
|
"lng_min": 36.25, "lng_max": 36.35
|
||||||
|
},
|
||||||
|
"Aleppo (Syria)": {
|
||||||
|
"lat_min": 36.15, "lat_max": 36.25,
|
||||||
|
"lng_min": 37.10, "lng_max": 37.20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# =======================================================
|
||||||
|
|
||||||
|
def generate_random_route():
|
||||||
|
"""Generates random starting and ending points inside Jordan and Syria populated cities."""
|
||||||
|
region_name = random.choice(list(REGIONS.keys()))
|
||||||
|
bbox = REGIONS[region_name]
|
||||||
|
|
||||||
|
# Pick a random starting point in the selected region
|
||||||
|
from_lat = random.uniform(bbox["lat_min"], bbox["lat_max"])
|
||||||
|
from_lng = random.uniform(bbox["lng_min"], bbox["lng_max"])
|
||||||
|
|
||||||
|
# Pick a destination within the same region (~10-15km max) to ensure a quick and valid route
|
||||||
|
to_lat = from_lat + random.uniform(-0.08, 0.08)
|
||||||
|
to_lng = from_lng + random.uniform(-0.08, 0.08)
|
||||||
|
|
||||||
|
# Clip coordinates to region bounds
|
||||||
|
to_lat = max(bbox["lat_min"], min(to_lat, bbox["lat_max"]))
|
||||||
|
to_lng = max(bbox["lng_min"], min(to_lng, bbox["lng_max"]))
|
||||||
|
|
||||||
|
return region_name, from_lat, from_lng, to_lat, to_lng
|
||||||
|
|
||||||
|
def send_request(request_id):
|
||||||
|
region_name, from_lat, from_lng, to_lat, to_lng = generate_random_route()
|
||||||
|
|
||||||
|
# Construct dynamic URL with random coordinates
|
||||||
|
url = (
|
||||||
|
f"{BASE_URL}?fromLat={from_lat:.5f}&fromLng={from_lng:.5f}"
|
||||||
|
f"&toLat={to_lat:.5f}&toLng={to_lng:.5f}&locale=ar"
|
||||||
|
)
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
headers={
|
||||||
|
"x-api-key": API_KEY,
|
||||||
|
"User-Agent": "Benchmark-Client/1.0"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
status_code = 0
|
||||||
|
error_message = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=TIMEOUT_SECONDS) as response:
|
||||||
|
status_code = response.status
|
||||||
|
response.read()
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
status_code = e.code
|
||||||
|
try:
|
||||||
|
err_body = e.read().decode('utf-8')
|
||||||
|
error_message = f"HTTP {e.code}: {json.loads(err_body).get('message', e.reason)}"
|
||||||
|
except Exception:
|
||||||
|
error_message = f"HTTP Error {e.code}: {e.reason}"
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
status_code = 0
|
||||||
|
error_message = f"URL Error: {e.reason}"
|
||||||
|
except Exception as e:
|
||||||
|
status_code = 0
|
||||||
|
error_message = f"Generic Error: {str(e)}"
|
||||||
|
|
||||||
|
end_time = time.perf_counter()
|
||||||
|
latency = (end_time - start_time) * 1000.0 # Convert to milliseconds
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": request_id,
|
||||||
|
"region": region_name,
|
||||||
|
"success": 200 <= status_code < 300,
|
||||||
|
"status_code": status_code,
|
||||||
|
"latency": latency,
|
||||||
|
"error": error_message
|
||||||
|
}
|
||||||
|
|
||||||
|
def print_report(results, elapsed_time):
|
||||||
|
latencies = [r["latency"] for r in results]
|
||||||
|
successes = [r for r in results if r["success"]]
|
||||||
|
failures = [r for r in results if not r["success"]]
|
||||||
|
|
||||||
|
latencies.sort()
|
||||||
|
|
||||||
|
total_reqs = len(results)
|
||||||
|
success_count = len(successes)
|
||||||
|
failure_count = len(failures)
|
||||||
|
|
||||||
|
avg_latency = sum(latencies) / total_reqs if total_reqs > 0 else 0
|
||||||
|
min_latency = latencies[0] if latencies else 0
|
||||||
|
max_latency = latencies[-1] if latencies else 0
|
||||||
|
|
||||||
|
def percentile(p):
|
||||||
|
if not latencies:
|
||||||
|
return 0
|
||||||
|
idx = int(len(latencies) * p)
|
||||||
|
return latencies[min(idx, len(latencies) - 1)]
|
||||||
|
|
||||||
|
rps = total_reqs / elapsed_time if elapsed_time > 0 else 0
|
||||||
|
|
||||||
|
# Calculate stats per region
|
||||||
|
region_stats = {}
|
||||||
|
for r in results:
|
||||||
|
reg = r["region"]
|
||||||
|
if reg not in region_stats:
|
||||||
|
region_stats[reg] = {"total": 0, "success": 0, "latencies": []}
|
||||||
|
region_stats[reg]["total"] += 1
|
||||||
|
if r["success"]:
|
||||||
|
region_stats[reg]["success"] += 1
|
||||||
|
region_stats[reg]["latencies"].append(r["latency"])
|
||||||
|
|
||||||
|
print("\n" + "="*50)
|
||||||
|
print(" API LOAD TESTING REPORT ")
|
||||||
|
print("="*50)
|
||||||
|
print(f"Target URL: {BASE_URL}")
|
||||||
|
print(f"Concurrency Level: {CONCURRENCY} threads")
|
||||||
|
print(f"Total Requests: {total_reqs}")
|
||||||
|
print(f"Time Taken: {elapsed_time:.3f} seconds")
|
||||||
|
print(f"Successful Requests: {success_count} ({success_count/total_reqs*100:.1f}%)")
|
||||||
|
print(f"Failed Requests: {failure_count} ({failure_count/total_reqs*100:.1f}%)")
|
||||||
|
print(f"Requests per Second: {rps:.2f} RPS")
|
||||||
|
print("-"*50)
|
||||||
|
print("PER-REGION SUMMARY:")
|
||||||
|
for region, stats in region_stats.items():
|
||||||
|
r_avg = sum(stats["latencies"]) / stats["total"] if stats["total"] > 0 else 0
|
||||||
|
print(f" {region:17}: {stats['total']} reqs, Avg: {r_avg:.1f}ms, Success: {stats['success']}/{stats['total']}")
|
||||||
|
print("-"*50)
|
||||||
|
print("LATENCY STATISTICS (ms):")
|
||||||
|
print(f" Min: {min_latency:.2f} ms")
|
||||||
|
print(f" Max: {max_latency:.2f} ms")
|
||||||
|
print(f" Average: {avg_latency:.2f} ms")
|
||||||
|
print(f" Median (50%): {percentile(0.50):.2f} ms")
|
||||||
|
print(f" 90th Percentile: {percentile(0.90):.2f} ms")
|
||||||
|
print(f" 95th Percentile: {percentile(0.95):.2f} ms")
|
||||||
|
print(f" 99th Percentile: {percentile(0.99):.2f} ms")
|
||||||
|
print("="*50)
|
||||||
|
|
||||||
|
if failure_count > 0:
|
||||||
|
print("\nERROR SUMMARY:")
|
||||||
|
errors = {}
|
||||||
|
for f in failures:
|
||||||
|
err = f["error"] or f"HTTP {f['status_code']}"
|
||||||
|
errors[err] = errors.get(err, 0) + 1
|
||||||
|
for err, count in errors.items():
|
||||||
|
print(f" - {err}: {count} occurrence(s)")
|
||||||
|
print("="*50)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"Starting dynamic benchmark of: {BASE_URL}")
|
||||||
|
print(f"Sending {TOTAL_REQUESTS} randomized requests (Amman, Irbid, Damascus, Aleppo)...")
|
||||||
|
print(f"Concurrency Level: {CONCURRENCY} concurrent threads")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=CONCURRENCY) as executor:
|
||||||
|
futures = {executor.submit(send_request, i): i for i in range(TOTAL_REQUESTS)}
|
||||||
|
|
||||||
|
completed = 0
|
||||||
|
for future in as_completed(futures):
|
||||||
|
results.append(future.result())
|
||||||
|
completed += 1
|
||||||
|
if completed % (TOTAL_REQUESTS // 10 or 1) == 0 or completed == TOTAL_REQUESTS:
|
||||||
|
print(f"Progress: {completed}/{TOTAL_REQUESTS} requests completed...")
|
||||||
|
|
||||||
|
end_time = time.perf_counter()
|
||||||
|
elapsed_time = end_time - start_time
|
||||||
|
|
||||||
|
print_report(results, elapsed_time)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
17
scratch/test_api.py
Normal file
17
scratch/test_api.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
|
||||||
|
def check_country(country):
|
||||||
|
url = "https://api.intaleq.xyz/intaleq_v3/ride/kazan/get.php"
|
||||||
|
data = json.dumps({"country": country}).encode('utf-8')
|
||||||
|
req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json'})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req) as response:
|
||||||
|
res = response.read().decode('utf-8')
|
||||||
|
print(f"Country: {country} -> {res}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Country: {country} -> Error: {e}")
|
||||||
|
|
||||||
|
check_country("Jordan")
|
||||||
|
check_country("Syria")
|
||||||
|
check_country("Egypt")
|
||||||
Reference in New Issue
Block a user