25-10-5/1

This commit is contained in:
Hamza-Ayed
2025-10-05 14:12:23 +03:00
parent 729378c507
commit f5dfe2c0fe
19 changed files with 1332 additions and 970 deletions

View File

@@ -47,8 +47,8 @@ android {
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 29 minSdk = 29
targetSdk = 36 targetSdk = 36
versionCode = 19 versionCode = 21
versionName = '1.0.19' versionName = '1.0.21'
multiDexEnabled = true multiDexEnabled = true
ndk { ndk {
abiFilters "armeabi-v7a", "arm64-v8a" abiFilters "armeabi-v7a", "arm64-v8a"

File diff suppressed because one or more lines are too long

View File

@@ -33,11 +33,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>9</string> <string>10</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0.9</string> <string>1.0.10</string>
<key>FirebaseAppDelegateProxyEnabled</key> <key>FirebaseAppDelegateProxyEnabled</key>
<string>NO</string> <string>NO</string>
<key>GMSApiKey</key> <key>GMSApiKey</key>

39
lib/app_bindings.dart Normal file
View File

@@ -0,0 +1,39 @@
import 'package:get/get.dart';
import 'package:Intaleq/controller/auth/login_controller.dart';
import 'package:Intaleq/controller/firebase/firbase_messge.dart';
import 'package:Intaleq/controller/firebase/local_notification.dart';
import 'package:Intaleq/controller/home/deep_link_controller.dart';
import 'package:Intaleq/controller/local/local_controller.dart';
/// This is the central dependency injection file for the app.
/// It uses GetX Bindings to make the app start faster and manage memory better.
class AppBindings extends Bindings {
@override
void dependencies() {
// --- [Type 1: Permanent Controllers] ---
// Use Get.put() for controllers that need to be available immediately and ALWAYS.
// They are created right away and never destroyed.
// LocaleController is needed instantly to set the app's theme and language.
Get.put(LocaleController());
// DeepLinkController must always be listening for incoming links.
// `permanent: true` ensures it survives `Get.offAll()`.
Get.put(DeepLinkController(), permanent: true);
// --- [Type 2: Lazy Loaded "Phoenix" Controllers] ---
// Use Get.lazyPut() for controllers that are heavy or not needed immediately.
// They are only created in memory the first time `Get.find()` is called.
// `fenix: true` is the key: it allows the controller to be "reborn" after being
// destroyed (e.g., by Get.offAll), preserving its state and functionality.
// LoginController is only needed during the login process on the splash screen.
Get.lazyPut(() => LoginController(), fenix: true);
// NotificationController is initialized on the splash screen, but might be needed later.
Get.lazyPut(() => NotificationController(), fenix: true);
// FirebaseMessagesController is also initialized on splash, but must persist its token and listeners.
Get.lazyPut(() => FirebaseMessagesController(), fenix: true);
}
}

View File

@@ -160,9 +160,9 @@ class LoginController extends GetxController {
Uri.parse(AppLink.loginJwtRider), Uri.parse(AppLink.loginJwtRider),
body: payload, body: payload,
); );
// Log.print('req: ${response1.request}'); Log.print('req: ${response1.request}');
// Log.print('response: ${response1.body}'); Log.print('response: ${response1.body}');
// Log.print('payload: ${payload}'); Log.print('payload: ${payload}');
// Log.print('decodedResponse1: ${jsonDecode(response1.body)}'); // Log.print('decodedResponse1: ${jsonDecode(response1.body)}');
if (response1.statusCode == 200) { if (response1.statusCode == 200) {

View File

@@ -69,7 +69,9 @@ class FirebaseMessagesController extends GetxController {
} }
NotificationController notificationController = NotificationController notificationController =
Get.put(NotificationController()); Get.isRegistered<NotificationController>()
? Get.find<NotificationController>()
: Get.put(NotificationController());
Future getTokens() async { Future getTokens() async {
String? basicAuthCredentials = String? basicAuthCredentials =
@@ -96,7 +98,7 @@ class FirebaseMessagesController extends GetxController {
Future getToken() async { Future getToken() async {
fcmToken.getToken().then((token) { fcmToken.getToken().then((token) {
Log.print('fcmToken: ${token}'); // Log.print('fcmToken: ${token}');
box.write(BoxName.tokenFCM, (token.toString())); box.write(BoxName.tokenFCM, (token.toString()));
}); });
@@ -125,16 +127,17 @@ class FirebaseMessagesController extends GetxController {
Future<void> fireBaseTitles(RemoteMessage message) async { Future<void> fireBaseTitles(RemoteMessage message) async {
if (message.notification!.title! == 'Order'.tr) { if (message.notification!.title! == 'Order'.tr) {
Log.print('message: ${message}');
if (Platform.isAndroid) { if (Platform.isAndroid) {
notificationController.showNotification( notificationController.showNotification(
'Order', message.notification!.body!, 'Order'); 'Order'.tr, message.notification!.body!, 'Order');
} }
} else if (message.notification!.title! == 'Accepted Ride') { } else if (message.notification!.title! == 'Accepted Ride') {
if (Platform.isAndroid) { if (Platform.isAndroid) {
notificationController.showNotification( notificationController.showNotification(
'Accepted Ride'.tr, 'Driver Accepted the Ride for You'.tr, 'ding'); 'Accepted Ride'.tr, 'Driver Accepted the Ride for You'.tr, 'ding');
} }
var passengerList = message.data['DriverList']; var passengerList = message.data['passengerList'];
var myList = jsonDecode(passengerList) as List<dynamic>; var myList = jsonDecode(passengerList) as List<dynamic>;
Log.print('myList: ${myList}'); Log.print('myList: ${myList}');
@@ -513,7 +516,7 @@ class FirebaseMessagesController extends GetxController {
// 'DriverList': jsonEncode(data), // 'DriverList': jsonEncode(data),
// }, // },
'android': { 'android': {
'priority': 'high', // Set priority to high 'priority': 'HIGH ', // Set priority to high
'notification': { 'notification': {
'sound': tone, 'sound': tone,
}, },
@@ -549,7 +552,7 @@ class FirebaseMessagesController extends GetxController {
// 'body': body, // 'body': body,
// 'sound': 'true' // 'sound': 'true'
// }, // },
// 'priority': 'high', // 'priority': 'HIGH ',
// 'data': <String, dynamic>{ // 'data': <String, dynamic>{
// 'click_action': 'FLUTTER_NOTIFICATION_CLICK', // 'click_action': 'FLUTTER_NOTIFICATION_CLICK',
// 'id': '1', // 'id': '1',
@@ -581,7 +584,7 @@ class FirebaseMessagesController extends GetxController {
Future<void> sendNotificationToDriverMAP( Future<void> sendNotificationToDriverMAP(
String title, String body, String token, List<String> data, String tone, String title, String body, String token, List<String> data, String tone,
{int retryCount = 2}) async { {int retryCount = 1}) async {
try { try {
if (serviceAccountKeyJson.isEmpty) { if (serviceAccountKeyJson.isEmpty) {
print("🔴 Error: Service Account Key is empty"); print("🔴 Error: Service Account Key is empty");
@@ -590,8 +593,8 @@ class FirebaseMessagesController extends GetxController {
// Initialize AccessTokenManager // Initialize AccessTokenManager
final accessTokenManager = AccessTokenManager(serviceAccountKeyJson); final accessTokenManager = AccessTokenManager(serviceAccountKeyJson);
Log.print( // Log.print(
'accessTokenManager: ${accessTokenManager.serviceAccountJsonKey}'); // 'accessTokenManager: ${accessTokenManager.serviceAccountJsonKey}');
// Obtain an OAuth 2.0 access token // Obtain an OAuth 2.0 access token
final accessToken = await accessTokenManager.getAccessToken(); final accessToken = await accessTokenManager.getAccessToken();
@@ -616,7 +619,7 @@ class FirebaseMessagesController extends GetxController {
'DriverList': jsonEncode(data), 'DriverList': jsonEncode(data),
}, },
'android': { 'android': {
'priority': 'high', // Set priority to high 'priority': 'HIGH ', // Set priority to high
'notification': { 'notification': {
'sound': tone, 'sound': tone,
}, },
@@ -693,7 +696,7 @@ class FirebaseMessagesController extends GetxController {
'body': body, 'body': body,
}, },
'android': { 'android': {
'priority': 'high', // Set priority to high 'priority': 'HIGH ', // Set priority to high
'notification': { 'notification': {
'sound': tone, 'sound': tone,
}, },

View File

@@ -0,0 +1,71 @@
import 'package:http/http.dart' as http;
import 'dart:convert';
class NotificationService {
// استبدل هذا الرابط بالرابط الصحيح لملف PHP على السيرفر الخاص بك
static const String _serverUrl =
'https://syria.intaleq.xyz/intaleq/fcm/send_fcm.php';
/// Sends a notification via your backend server.
///
/// [target]: The device token or the topic name.
/// [title]: The notification title.
/// [body]: The notification body.
/// [isTopic]: Set to true if the target is a topic, false if it's a device token.
static Future<void> sendNotification({
required String target,
required String title,
required String body,
bool isTopic = false,
}) async {
try {
final response = await http.post(
Uri.parse(_serverUrl),
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode({
'target': target,
'title': title,
'body': body,
'isTopic': isTopic,
}),
);
if (response.statusCode == 200) {
print('Notification sent successfully.');
print('Server Response: ${response.body}');
} else {
print(
'Failed to send notification. Status code: ${response.statusCode}');
print('Server Error: ${response.body}');
}
} catch (e) {
print('An error occurred while sending notification: $e');
}
}
}
// --- Example of how to use it ---
// To send to a specific driver (using their token)
void sendToSpecificDriver() {
String driverToken =
'bk3RNwTe3H0:CI2k_HHwgIpoDKCI5oT...'; // The driver's FCM token
NotificationService.sendNotification(
target: driverToken,
title: 'New Trip Request!',
body: 'A passenger is waiting for you.',
isTopic: false, // Important: this is a token
);
}
// To send to all drivers (using a topic)
void sendToAllDrivers() {
NotificationService.sendNotification(
target: 'drivers', // The name of the topic
title: 'Important Announcement',
body: 'Please update your app to the latest version.',
isTopic: true, // Important: this is a topic
);
}

View File

@@ -123,6 +123,7 @@ class CRUD {
// 401 → دع الطبقة العليا تتعامل مع التجديد // 401 → دع الطبقة العليا تتعامل مع التجديد
if (sc == 401) { if (sc == 401) {
await Get.put(LoginController()).getJWT();
// لا تستدع getJWT هنا كي لا نضاعف الرحلات // لا تستدع getJWT هنا كي لا نضاعف الرحلات
return 'token_expired'; return 'token_expired';
} }
@@ -190,6 +191,10 @@ class CRUD {
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}' 'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}'
}, },
); );
Log.print('response.body: ${response.body}');
Log.print('response.request: ${response.request}');
Log.print('response.payload: ${payload}');
if (response.statusCode == 200) { if (response.statusCode == 200) {
var jsonData = jsonDecode(response.body); var jsonData = jsonDecode(response.body);
if (jsonData['status'] == 'success') { if (jsonData['status'] == 'success') {

View File

@@ -0,0 +1,62 @@
import 'package:google_maps_flutter/google_maps_flutter.dart';
List<LatLng> decodePolylineIsolate(String encoded) {
List<LatLng> points = [];
int index = 0, len = encoded.length;
int lat = 0, lng = 0;
while (index < len) {
int b, shift = 0, result = 0;
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lat += dlat;
shift = 0;
result = 0;
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lng += dlng;
points.add(LatLng(lat / 1E5, lng / 1E5));
}
return points;
}
// Helper method for decoding polyline (if not already defined)
// List<LatLng> decodePolyline(String encoded) {
// List<LatLng> points = [];
// int index = 0, len = encoded.length;
// int lat = 0, lng = 0;
// while (index < len) {
// int b, shift = 0, result = 0;
// do {
// b = encoded.codeUnitAt(index++) - 63;
// result |= (b & 0x1f) << shift;
// shift += 5;
// } while (b >= 0x20);
// int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
// lat += dlat;
// shift = 0;
// result = 0;
// do {
// b = encoded.codeUnitAt(index++) - 63;
// result |= (b & 0x1f) << shift;
// shift += 5;
// } while (b >= 0x20);
// int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
// lng += dlng;
// points.add(LatLng(lat / 1E5, lng / 1E5));
// }
// return points;
// }

View File

@@ -6,6 +6,7 @@ import 'dart:math' as math;
import 'dart:ui'; import 'dart:ui';
import 'dart:convert'; import 'dart:convert';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:Intaleq/constant/univeries_polygon.dart'; import 'package:Intaleq/constant/univeries_polygon.dart';
@@ -22,7 +23,7 @@ import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_polyline_algorithm/google_polyline_algorithm.dart'; // import 'package:google_polyline_algorithm/google_polyline_algorithm.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:location/location.dart'; import 'package:location/location.dart';
import 'package:Intaleq/constant/colors.dart'; import 'package:Intaleq/constant/colors.dart';
@@ -52,6 +53,7 @@ import '../functions/launch.dart';
import '../functions/package_info.dart'; import '../functions/package_info.dart';
import '../functions/secure_storage.dart'; import '../functions/secure_storage.dart';
import '../payment/payment_controller.dart'; import '../payment/payment_controller.dart';
import 'decode_polyline_isolate.dart';
import 'deep_link_controller.dart'; import 'deep_link_controller.dart';
import 'device_tier.dart'; import 'device_tier.dart';
import 'vip_waitting_page.dart'; import 'vip_waitting_page.dart';
@@ -1035,7 +1037,7 @@ class MapPassengerController extends GetxController {
if (isBeginRideFromDriverRunning) return; // Prevent duplicate streams if (isBeginRideFromDriverRunning) return; // Prevent duplicate streams
isBeginRideFromDriverRunning = true; isBeginRideFromDriverRunning = true;
Timer.periodic(const Duration(seconds: 1), (timer) async { Timer.periodic(const Duration(seconds: 2), (timer) async {
try { try {
var res = await CRUD().get( var res = await CRUD().get(
link: AppLink.getRideStatusBegin, payload: {'ride_id': rideId}); link: AppLink.getRideStatusBegin, payload: {'ride_id': rideId});
@@ -1138,13 +1140,14 @@ class MapPassengerController extends GetxController {
Map<String, dynamic> tripData = Map<String, dynamic> tripData =
box.read(BoxName.tripData) as Map<String, dynamic>; box.read(BoxName.tripData) as Map<String, dynamic>;
final points = decodePolyline( String pointsString =
tripData["routes"][0]["overview_polyline"]["points"]); (tripData["routes"][0]["overview_polyline"]["points"]);
for (int i = 0; i < points.length; i++) { List<LatLng> decodedPoints =
double lat = points[i][0].toDouble(); await compute(decodePolylineIsolate, pointsString);
double lng = points[i][1].toDouble(); // decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
polylineCoordinates.add(LatLng(lat, lng)); for (int i = 0; i < decodedPoints.length; i++) {
polylineCoordinates.add(decodedPoints[i]);
} }
var polyline = Polyline( var polyline = Polyline(
polylineId: const PolylineId('begin trip'), polylineId: const PolylineId('begin trip'),
@@ -1819,6 +1822,7 @@ class MapPassengerController extends GetxController {
await getCarsLocationByPassengerAndReloadMarker( await getCarsLocationByPassengerAndReloadMarker(
box.read(BoxName.carType), reloadDuration); box.read(BoxName.carType), reloadDuration);
// await getNearestDriverByPassengerLocation(); // await getNearestDriverByPassengerLocation();
currentTimeSearchingCaptainWindow = 0;
driversStatusForSearchWindow = 'We are search for nearst driver'.tr; driversStatusForSearchWindow = 'We are search for nearst driver'.tr;
if (isDriversDataValid()) { if (isDriversDataValid()) {
driversFound = true; driversFound = true;
@@ -2142,8 +2146,9 @@ class MapPassengerController extends GetxController {
} else if (rideStatusDelayed == 'Apply' || } else if (rideStatusDelayed == 'Apply' ||
rideStatusDelayed == 'Applied') { rideStatusDelayed == 'Applied') {
isApplied = true; isApplied = true;
rideAppliedFromDriver(isApplied);
timer.cancel(); timer.cancel();
rideAppliedFromDriver(isApplied);
// timer.cancel();
// Close stream after applying // Close stream after applying
} else if (attemptCounter >= maxAttempts || } else if (attemptCounter >= maxAttempts ||
rideStatusDelayed == 'waiting') { rideStatusDelayed == 'waiting') {
@@ -2186,7 +2191,7 @@ class MapPassengerController extends GetxController {
rideAppliedFromDriver(bool isApplied) async { rideAppliedFromDriver(bool isApplied) async {
await getUpdatedRideForDriverApply(rideId); await getUpdatedRideForDriverApply(rideId);
NotificationController().showNotification( NotificationController().showNotification(
'Accepted Ride'.tr, 'Accepted Ride',
'$driverName ${'accepted your order at price'.tr} ${totalPassenger.toStringAsFixed(1)} ${'with type'.tr} ${box.read(BoxName.carType)}', '$driverName ${'accepted your order at price'.tr} ${totalPassenger.toStringAsFixed(1)} ${'with type'.tr} ${box.read(BoxName.carType)}',
'ding'); 'ding');
if (box.read(BoxName.carType) == 'Speed' || if (box.read(BoxName.carType) == 'Speed' ||
@@ -2306,7 +2311,7 @@ class MapPassengerController extends GetxController {
// driversToken.remove(driverToken); // driversToken.remove(driverToken);
// for (var i = 1; i < driversToken.length; i++) { // for (var i = 1; i < driversToken.length; i++) {
firebaseMessagesController.sendNotificationToDriverMAP( firebaseMessagesController.sendNotificationToDriverMAP(
'Order Accepted'.tr, 'Order Accepted',
'$driverName${'Accepted your order'.tr}', '$driverName${'Accepted your order'.tr}',
driverToken.toString(), driverToken.toString(),
[], [],
@@ -3514,57 +3519,71 @@ class MapPassengerController extends GetxController {
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض
double _haversineKm(double lat1, double lon1, double lat2, double lon2) { // double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
const R = 6371.0; // km // const R = 6371.0; // km
final dLat = (lat2 - lat1) * math.pi / 180.0; // final dLat = (lat2 - lat1) * math.pi / 180.0;
final dLon = (lon2 - lon1) * math.pi / 180.0; // final dLon = (lon2 - lon1) * math.pi / 180.0;
final a = math.sin(dLat / 2) * math.sin(dLat / 2) + // final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
math.cos(lat1 * math.pi / 180.0) * // math.cos(lat1 * math.pi / 180.0) *
math.cos(lat2 * math.pi / 180.0) * // math.cos(lat2 * math.pi / 180.0) *
math.sin(dLon / 2) * // math.sin(dLon / 2) *
math.sin(dLon / 2); // math.sin(dLon / 2);
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); // final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
return R * c; // return R * c;
} // }
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض
double _kmToLatDelta(double km) => km / 111.0; // double _kmToLatDelta(double km) => km / 111.0;
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض) // /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض)
double _kmToLngDelta(double km, double atLat) => // double _kmToLngDelta(double km, double atLat) =>
km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9); // km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9);
/// حساب درجة التطابق النصي (كل كلمة تبدأ بها الاسم = 2 نقاط، يحتويها = 1 نقطة) /// حساب درجة التطابق النصي (كل كلمة تبدأ بها الاسم = 2 نقاط، يحتويها = 1 نقطة)
double _relevanceScore(String name, String query) { // double _relevanceScore(String name, String query) {
final n = name.toLowerCase(); // final n = name.toLowerCase();
final parts = // final parts =
query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2); // query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2);
double s = 0.0; // double s = 0.0;
for (final p in parts) { // for (final p in parts) {
if (n.startsWith(p)) { // if (n.startsWith(p)) {
s += 2.0; // s += 2.0;
} else if (n.contains(p)) { // } else if (n.contains(p)) {
s += 1.0; // s += 1.0;
} // }
} // }
return s; // return s;
} // }
// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها
// انسخ هذه الدوال والصقها داخل كلاس الكنترولر الخاص بك
// -----------------------------------------------------------------
// --== الدالة الرئيسية للبحث ==--
// -----------------------------------------------------------------
/// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها
// انسخ هذه الدوال والصقها داخل كلاس الكنترولر الخاص بك
// -----------------------------------------------------------------
// --== الدالة الرئيسية للبحث ==--
// -----------------------------------------------------------------
/// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها
Future<void> getPlaces() async { Future<void> getPlaces() async {
// افترض وجود `placeDestinationController` و `passengerLocation` و `CRUD()` معرفة في الكنترولر
final q = placeDestinationController.text.trim(); final q = placeDestinationController.text.trim();
if (q.isEmpty) { if (q.isEmpty || q.length < 3) {
// يفضل عدم البحث قبل 3 أحرف
placesDestination = []; placesDestination = [];
update(); update(); // افترض أنك تستخدم GetX أو أي State Management آخر
return; return;
} }
final lat = passengerLocation.latitude; final lat = passengerLocation.latitude;
final lng = passengerLocation.longitude; final lng = passengerLocation.longitude;
// نصف قطر البحث بالكيلومتر (عدّل حسب رغبتك) // نصف قطر البحث بالكيلومتر
const radiusKm = 200.0; const radiusKm = 200.0;
// حساب الباوند الصحيح (درجات، وليس 2.2 درجة ثابتة) // حساب النطاق الجغرافي (Bounding Box) لإرساله للسيرفر
final latDelta = _kmToLatDelta(radiusKm); final latDelta = _kmToLatDelta(radiusKm);
final lngDelta = _kmToLngDelta(radiusKm, lat); final lngDelta = _kmToLngDelta(radiusKm, lat);
@@ -3574,6 +3593,7 @@ class MapPassengerController extends GetxController {
final lngMax = lng + lngDelta; final lngMax = lng + lngDelta;
try { try {
// استدعاء الـ API (تأكد من أن AppLink.getPlacesSyria يشير للسكريبت الجديد)
final response = await CRUD().post( final response = await CRUD().post(
link: AppLink.getPlacesSyria, link: AppLink.getPlacesSyria,
payload: { payload: {
@@ -3585,59 +3605,106 @@ class MapPassengerController extends GetxController {
}, },
); );
// يدعم شكلي استجابة: إما {"...","message":[...]} أو قائمة مباشرة [...] // --- [تم الإصلاح هنا] ---
// معالجة الاستجابة من السيرفر بشكل يوافق {"status":"success", "message":[...]}
List list; List list;
if (response is Map && response['message'] is List) { if (response is Map) {
list = List.from(response['message'] as List); if (response['status'] == 'success' && response['message'] is List) {
list = List.from(response['message'] as List);
} else if (response['status'] == 'failure') {
print('Server Error: ${response['message']}');
return;
} else {
print('Unexpected Map shape from server');
return;
}
} else if (response is List) { } else if (response is List) {
// للتعامل مع الحالات التي قد يرجع فيها السيرفر قائمة مباشرة
list = List.from(response); list = List.from(response);
} else { } else {
print('Unexpected response shape'); print('Unexpected response shape from server');
return; return;
} }
// جهّز الحقول المحتملة للأسماء // --- هنا يبدأ عمل فلاتر: الترتيب النهائي الدقيق ---
// دالة مساعدة لاختيار أفضل اسم متاح
String _bestName(Map p) { String _bestName(Map p) {
return (p['name'] ?? p['name_ar'] ?? p['name_en'] ?? '').toString(); return (p['name_ar'] ?? p['name'] ?? p['name_en'] ?? '').toString();
} }
// احسب المسافة ودرجة التطابق والنقاط // حساب المسافة والصلة والنقاط النهائية لكل نتيجة
for (final p in list) { for (final p in list) {
final plat = double.tryParse(p['latitude']?.toString() ?? '') ?? 0.0; final plat = double.tryParse(p['latitude']?.toString() ?? '0.0') ?? 0.0;
final plng = double.tryParse(p['longitude']?.toString() ?? '') ?? 0.0; final plng =
double.tryParse(p['longitude']?.toString() ?? '0.0') ?? 0.0;
final d = _haversineKm(lat, lng, plat, plng); final distance = _haversineKm(lat, lng, plat, plng);
final rel = _relevanceScore(_bestName(p), q); final relevance = _relevanceScore(_bestName(p), q);
// معادلة ترتيب ذكية: مسافة أقل + تطابق أعلى = نقاط أعلى // معادلة الترتيب: (الأولوية للمسافة الأقرب) * (ثم الصلة الأعلى)
// تضيف +1 لضمان عدم وصول الوزن للصفر عند عدم وجود تطابق final score = (1.0 / (1.0 + distance)) * (1.0 + relevance);
final score = (1.0 / (1.0 + d)) * (1.0 + rel);
p['distanceKm'] = d; p['distanceKm'] = distance;
p['relevance'] = rel; p['relevance'] = relevance;
p['score'] = score; p['score'] = score;
} }
// رتّب حسب score تنازليًا، ثم المسافة تصاعديًا كحسم // ترتيب القائمة النهائية حسب النقاط (الأعلى أولاً)
list.sort((a, b) { list.sort((a, b) {
final sa = (a['score'] ?? 0.0) as double; final sa = (a['score'] ?? 0.0) as double;
final sb = (b['score'] ?? 0.0) as double; final sb = (b['score'] ?? 0.0) as double;
final cmp = sb.compareTo(sa); return sb.compareTo(sa);
if (cmp != 0) return cmp;
final da = (a['distanceKm'] ?? 1e9) as double;
final db = (b['distanceKm'] ?? 1e9) as double;
return da.compareTo(db);
}); });
// خذ أول 1015 للعرض (اختياري)، أو اعرض الكل placesDestination = list;
placesDestination = list.take(15).toList(); print('Updated places: $placesDestination');
Log.print('placesDestination: $placesDestination');
update(); update();
} catch (e) { } catch (e) {
print('Exception in getPlaces: $e'); print('Exception in getPlaces: $e');
} }
} }
// -----------------------------------------------------------------
// --== دوال مساعدة ==--
// -----------------------------------------------------------------
/// تحسب المسافة بين نقطتين بالكيلومتر (معادلة هافرساين)
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
const R = 6371.0; // نصف قطر الأرض بالكيلومتر
final dLat = (lat2 - lat1) * (pi / 180.0);
final dLon = (lon2 - lon1) * (pi / 180.0);
final rLat1 = lat1 * (pi / 180.0);
final rLat2 = lat2 * (pi / 180.0);
final a = sin(dLat / 2) * sin(dLat / 2) +
cos(rLat1) * cos(rLat2) * sin(dLon / 2) * sin(dLon / 2);
final c = 2 * atan2(sqrt(a), sqrt(1 - a));
return R * c;
}
/// تحسب درجة تطابق بسيطة بين اسم المكان وكلمة البحث
double _relevanceScore(String placeName, String query) {
if (placeName.isEmpty || query.isEmpty) return 0.0;
final pLower = placeName.toLowerCase();
final qLower = query.toLowerCase();
if (pLower.startsWith(qLower)) return 1.0; // تطابق كامل في البداية
if (pLower.contains(qLower)) return 0.5; // تحتوي على الكلمة
return 0.0;
}
/// تحويل كيلومتر إلى فرق درجات لخط العرض
double _kmToLatDelta(double km) {
const kmInDegree = 111.32;
return km / kmInDegree;
}
/// تحويل كيلومتر إلى فرق درجات لخط الطول (يعتمد على خط العرض الحالي)
double _kmToLngDelta(double km, double latitude) {
const kmInDegree = 111.32;
return km / (kmInDegree * cos(latitude * (pi / 180.0)));
}
// var languageCode; // var languageCode;
// // تحديد اللغة حسب الإدخال // // تحديد اللغة حسب الإدخال
@@ -3786,27 +3853,102 @@ class MapPassengerController extends GetxController {
// update(); // update();
// } // }
Future getPlacesStart() async { Future<void> getPlacesStart() async {
var languageCode = wayPoint0Controller.text; // افترض وجود `placeDestinationController` و `passengerLocation` و `CRUD()` معرفة في الكنترولر
final q = placeStartController.text.trim();
// Regular expression to check for English alphabet characters if (q.isEmpty || q.length < 3) {
final englishRegex = RegExp(r'[a-zA-Z]'); // يفضل عدم البحث قبل 3 أحرف
placesStart = [];
// Check if text contains English characters update(); // افترض أنك تستخدم GetX أو أي State Management آخر
if (englishRegex.hasMatch(languageCode)) { return;
languageCode = 'en';
} else {
languageCode = 'ar';
} }
var url = final lat = passengerLocation.latitude;
// '${AppLink.googleMapsLink}place/nearbysearch/json?location=${mylocation.longitude}&radius=25000&language=ar&keyword=&key=${placeController.text}${AK.mapAPIKEY}'; final lng = passengerLocation.longitude;
'${AppLink.googleMapsLink}place/nearbysearch/json?keyword=${placeStartController.text}&location=${passengerLocation.latitude},${passengerLocation.longitude}&radius=250000&language=$languageCode&key=${AK.mapAPIKEY.toString()}';
var response = await CRUD().getGoogleApi(link: url, payload: {}); // نصف قطر البحث بالكيلومتر
const radiusKm = 200.0;
placesStart = response['results']; // حساب النطاق الجغرافي (Bounding Box) لإرساله للسيرفر
update(); final latDelta = _kmToLatDelta(radiusKm);
final lngDelta = _kmToLngDelta(radiusKm, lat);
final latMin = lat - latDelta;
final latMax = lat + latDelta;
final lngMin = lng - lngDelta;
final lngMax = lng + lngDelta;
try {
// استدعاء الـ API (تأكد من أن AppLink.getPlacesSyria يشير للسكريبت الجديد)
final response = await CRUD().post(
link: AppLink.getPlacesSyria,
payload: {
'query': q,
'lat_min': latMin.toString(),
'lat_max': latMax.toString(),
'lng_min': lngMin.toString(),
'lng_max': lngMax.toString(),
},
);
// --- [تم الإصلاح هنا] ---
// معالجة الاستجابة من السيرفر بشكل يوافق {"status":"success", "message":[...]}
List list;
if (response is Map) {
if (response['status'] == 'success' && response['message'] is List) {
list = List.from(response['message'] as List);
} else if (response['status'] == 'failure') {
print('Server Error: ${response['message']}');
return;
} else {
print('Unexpected Map shape from server');
return;
}
} else if (response is List) {
// للتعامل مع الحالات التي قد يرجع فيها السيرفر قائمة مباشرة
list = List.from(response);
} else {
print('Unexpected response shape from server');
return;
}
// --- هنا يبدأ عمل فلاتر: الترتيب النهائي الدقيق ---
// دالة مساعدة لاختيار أفضل اسم متاح
String _bestName(Map p) {
return (p['name_ar'] ?? p['name'] ?? p['name_en'] ?? '').toString();
}
// حساب المسافة والصلة والنقاط النهائية لكل نتيجة
for (final p in list) {
final plat = double.tryParse(p['latitude']?.toString() ?? '0.0') ?? 0.0;
final plng =
double.tryParse(p['longitude']?.toString() ?? '0.0') ?? 0.0;
final distance = _haversineKm(lat, lng, plat, plng);
final relevance = _relevanceScore(_bestName(p), q);
// معادلة الترتيب: (الأولوية للمسافة الأقرب) * (ثم الصلة الأعلى)
final score = (1.0 / (1.0 + distance)) * (1.0 + relevance);
p['distanceKm'] = distance;
p['relevance'] = relevance;
p['score'] = score;
}
// ترتيب القائمة النهائية حسب النقاط (الأعلى أولاً)
list.sort((a, b) {
final sa = (a['score'] ?? 0.0) as double;
final sb = (b['score'] ?? 0.0) as double;
return sb.compareTo(sa);
});
placesStart = list;
print('Updated places: $placesDestination');
update();
} catch (e) {
print('Exception in getPlaces: $e');
}
} }
Future getPlacesListsWayPoint(int index) async { Future getPlacesListsWayPoint(int index) async {
@@ -4557,12 +4699,15 @@ class MapPassengerController extends GetxController {
data[0]["start_location"]['lat'], data[0]["start_location"]['lng']); data[0]["start_location"]['lat'], data[0]["start_location"]['lng']);
durationToRide = data[0]['duration']['value']; durationToRide = data[0]['duration']['value'];
final points = final String pointsString =
decodePolyline(response["routes"][0]["overview_polyline"]["points"]); response['routes'][0]["overview_polyline"]["points"];
for (int i = 0; i < points.length; i++) { List<LatLng> decodedPoints =
double lat = points[i][0].toDouble(); await compute(decodePolylineIsolate, pointsString);
double lng = points[i][1].toDouble(); // decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
polylineCoordinates.add(LatLng(lat, lng)); for (int i = 0; i < decodedPoints.length; i++) {
// double lat = decodedPoints[i][0].toDouble();
// double lng = decodedPoints[i][1].toDouble();
polylineCoordinates.add(decodedPoints[i]);
} }
// Define the northeast and southwest coordinates // Define the northeast and southwest coordinates
@@ -4609,18 +4754,18 @@ class MapPassengerController extends GetxController {
position: LatLng( position: LatLng(
data[0]["end_location"]['lat'], data[0]["end_location"]['lng']), data[0]["end_location"]['lat'], data[0]["end_location"]['lng']),
icon: endIcon, icon: endIcon,
// infoWindow: InfoWindow( infoWindow: InfoWindow(
// title: endNameAddress, title: endNameAddress,
// snippet: snippet:
// '$distance ${'KM'.tr} ⌛ ${hours > 0 ? '${'Your Ride Duration is '.tr}$hours ${'H and'.tr} $minutes ${'m'.tr}' : '${'Your Ride Duration is '.tr} $minutes ${'m'.tr}'}'), '$distance ${'KM'.tr}${hours > 0 ? '${'Your Ride Duration is '.tr}$hours ${'H and'.tr} $minutes ${'m'.tr}' : '${'Your Ride Duration is '.tr} $minutes ${'m'.tr}'}'),
), ),
); );
// // Show info windows automatically // // Show info windows automatically
// Future.delayed(const Duration(milliseconds: 500), () {
// mapController?.showMarkerInfoWindow(const MarkerId('start'));
// });
// Future.delayed(const Duration(milliseconds: 500), () { // Future.delayed(const Duration(milliseconds: 500), () {
// mapController?.showMarkerInfoWindow(const MarkerId('end')); // mapController?.showMarkerInfoWindow(const MarkerId('start'));
// });
// Future.delayed(const Duration(milliseconds: 500), () {
// mapController?.showMarkerInfoWindow(const MarkerId('end'));
// }); // });
// update(); // update();
@@ -4632,11 +4777,23 @@ class MapPassengerController extends GetxController {
false; // الأفضل أن يكون الافتراضي هو الجودة العالية false; // الأفضل أن يكون الافتراضي هو الجودة العالية
// نمرر عدد النقاط المناسب هنا // نمرر عدد النقاط المناسب هنا
animatePolylineLayered( if (Platform.isIOS) {
polylineCoordinates, animatePolylineLayered(
maxPoints: polylineCoordinates,
lowEndMode ? 30 : 150, // 30 نقطة لوضع الأداء، 150 للوضع العادي maxPoints:
); lowEndMode ? 30 : 150, // 30 نقطة لوضع الأداء، 150 للوضع العادي
);
} else {
polyLines.add(Polyline(
polylineId: const PolylineId('route'),
points: polylineCoordinates,
width: 6,
color: AppColor.primaryColor,
endCap: Cap.roundCap,
startCap: Cap.roundCap,
jointType: JointType.round,
));
}
rideConfirm = false; rideConfirm = false;
isMarkersShown = true; isMarkersShown = true;
@@ -4672,7 +4829,7 @@ class MapPassengerController extends GetxController {
// 2) رسم متدرّج بطبقات متراكبة (بدون حذف)، برونزي ↔ أخضر، مع zIndex وعرض مختلف // 2) رسم متدرّج بطبقات متراكبة (بدون حذف)، برونزي ↔ أخضر، مع zIndex وعرض مختلف
Future<void> animatePolylineLayered(List<LatLng> coordinates, Future<void> animatePolylineLayered(List<LatLng> coordinates,
{int layersCount = 1, int stepDelayMs = 10, int maxPoints = 150}) async { {int layersCount = 8, int stepDelayMs = 10, int maxPoints = 160}) async {
// امسح أي طبقات قديمة فقط الخاصة بالطريق // امسح أي طبقات قديمة فقط الخاصة بالطريق
polyLines.removeWhere((p) => p.polylineId.value.startsWith('route_layer_')); polyLines.removeWhere((p) => p.polylineId.value.startsWith('route_layer_'));
update(); update();
@@ -4682,8 +4839,8 @@ class MapPassengerController extends GetxController {
if (coords.length < 2) return; if (coords.length < 2) return;
// ألوان مع شفافية خفيفة للتمييز // ألوان مع شفافية خفيفة للتمييز
Color bronze([int alpha = 220]) => const Color(0xFFCD7F32).withAlpha(alpha); Color bronze([int alpha = 220]) => AppColor.gold;
Color green([int alpha = 220]) => Colors.green.withAlpha(alpha); Color green([int alpha = 220]) => AppColor.primaryColor;
Color _layerColor(int layer) => (layer % 2 == 0) ? bronze() : green(); Color _layerColor(int layer) => (layer % 2 == 0) ? bronze() : green();
@@ -4818,14 +4975,16 @@ class MapPassengerController extends GetxController {
distance = distanceOfDestination + (data[0]['distance']['value']) / 1000; distance = distanceOfDestination + (data[0]['distance']['value']) / 1000;
update(); update();
final points = // final points =
decodePolyline(response["routes"][0]["overview_polyline"]["points"]); // decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
for (int i = 0; i < points.length; i++) { final String pointsString =
if (points[i][0].toString() != '') { response['routes'][0]["overview_polyline"]["points"];
double lat = points[i][0].toDouble();
double lng = points[i][1].toDouble(); List<LatLng> decodedPoints =
polylineCoordinatesPointsAll[index].add(LatLng(lat, lng)); await compute(decodePolylineIsolate, pointsString);
} // decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
for (int i = 0; i < decodedPoints.length; i++) {
polylineCoordinates.add(decodedPoints[i]);
} }
// Define the northeast and southwest coordinates // Define the northeast and southwest coordinates

View File

@@ -1,4 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:Intaleq/controller/functions/crud.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@@ -7,22 +9,21 @@ import 'package:Intaleq/onbording_page.dart';
import 'package:Intaleq/views/auth/login_page.dart'; import 'package:Intaleq/views/auth/login_page.dart';
import 'package:Intaleq/controller/auth/login_controller.dart'; import 'package:Intaleq/controller/auth/login_controller.dart';
import 'package:Intaleq/controller/functions/secure_storage.dart'; import 'package:Intaleq/controller/functions/secure_storage.dart';
import 'package:Intaleq/views/widgets/my_scafold.dart'; import 'package:quick_actions/quick_actions.dart';
import 'package:Intaleq/constant/style.dart'; import '../../../constant/notification.dart';
import '../../../main.dart';
import '../firebase/firbase_messge.dart';
import '../firebase/local_notification.dart';
import '../functions/encrypt_decrypt.dart';
import '../../main.dart';
// كنترولر مع منطق تحميل محسن ينتظر العمليات غير المتزامنة
class SplashScreenController extends GetxController class SplashScreenController extends GetxController
with GetTickerProviderStateMixin { with GetTickerProviderStateMixin {
late AnimationController _animationController; late AnimationController _animationController;
late Animation<double> titleFadeAnimation,
// الحركات الخاصة بكل عنصر من عناصر الواجهة titleScaleAnimation,
late Animation<double> titleFadeAnimation; taglineFadeAnimation,
late Animation<double> titleScaleAnimation; footerFadeAnimation;
late Animation<double> taglineFadeAnimation;
late Animation<Offset> taglineSlideAnimation; late Animation<Offset> taglineSlideAnimation;
late Animation<double> footerFadeAnimation;
final progress = 0.0.obs; final progress = 0.0.obs;
Timer? _progressTimer; Timer? _progressTimer;
@@ -30,111 +31,160 @@ class SplashScreenController extends GetxController
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_animationController = AnimationController( _animationController = AnimationController(
vsync: this, vsync: this, duration: const Duration(milliseconds: 2000));
duration: const Duration(milliseconds: 2000),
);
// --- تعريف الحركات المتتالية --- // Animation definitions
titleFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( titleFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation( CurvedAnimation(
parent: _animationController, parent: _animationController,
curve: const Interval(0.0, 0.5, curve: Curves.easeOut), curve: const Interval(0.0, 0.5, curve: Curves.easeOut)));
),
);
titleScaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate( titleScaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
CurvedAnimation( CurvedAnimation(
parent: _animationController, parent: _animationController,
curve: const Interval(0.0, 0.5, curve: Curves.easeOut), curve: const Interval(0.0, 0.5, curve: Curves.easeOut)));
),
);
taglineFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( taglineFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation( CurvedAnimation(
parent: _animationController, parent: _animationController,
curve: const Interval(0.3, 0.8, curve: Curves.easeOut), curve: const Interval(0.3, 0.8, curve: Curves.easeOut)));
),
);
taglineSlideAnimation = taglineSlideAnimation =
Tween<Offset>(begin: const Offset(0, 0.5), end: Offset.zero).animate( Tween<Offset>(begin: const Offset(0, 0.5), end: Offset.zero).animate(
CurvedAnimation( CurvedAnimation(
parent: _animationController, parent: _animationController,
curve: const Interval(0.3, 0.8, curve: Curves.easeOut), curve: const Interval(0.3, 0.8, curve: Curves.easeOut)));
),
);
footerFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( footerFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation( CurvedAnimation(
parent: _animationController, parent: _animationController,
curve: const Interval(0.5, 1.0, curve: Curves.easeOut), curve: const Interval(0.5, 1.0, curve: Curves.easeOut)));
),
);
_animationController.forward(); _animationController.forward();
// بدء عملية التهيئة والتحميل
_initializeApp(); _initializeApp();
} }
/// تهيئة التطبيق وانتظار انتهاء التحميل قبل الانتقال
Future<void> _initializeApp() async { Future<void> _initializeApp() async {
// تشغيل مؤقت شريط التقدم ليعطي انطباعاً مرئياً فقط
_startProgressAnimation(); _startProgressAnimation();
// تعريف مهمتين: منطق التحميل، والحد الأدنى لوقت عرض الشاشة // Run navigation logic and background services in parallel.
final logicFuture = _performNavigationLogic(); final logicFuture = _performNavigationLogic();
final backgroundServicesFuture = _initializeBackgroundServices();
// Ensure the splash screen is visible for a minimum duration.
final minTimeFuture = Future.delayed(const Duration(seconds: 3)); final minTimeFuture = Future.delayed(const Duration(seconds: 3));
// الانتظار حتى انتهاء المهمتين معاً // Wait for all tasks to complete.
await Future.wait([logicFuture, minTimeFuture]); await Future.wait([logicFuture, backgroundServicesFuture, minTimeFuture]);
// بعد انتهاء الانتظار، سيتم الانتقال تلقائياً من داخل a_performNavigationLogic
} }
/// تشغيل حركة شريط التقدم بشكل مرئي ومنفصل عن منطق التحميل
void _startProgressAnimation() { void _startProgressAnimation() {
const totalTime = 2800; // مدة ملء الشريط // Visual timer for the progress bar.
const interval = 50; const totalTime = 2800; // ms
const interval = 50; // ms
int elapsed = 0; int elapsed = 0;
_progressTimer = _progressTimer =
Timer.periodic(const Duration(milliseconds: interval), (timer) { Timer.periodic(const Duration(milliseconds: interval), (timer) {
elapsed += interval; elapsed += interval;
progress.value = (elapsed / totalTime).clamp(0.0, 1.0); progress.value = (elapsed / totalTime).clamp(0.0, 1.0);
if (elapsed >= totalTime) { if (elapsed >= totalTime) timer.cancel();
timer.cancel();
}
}); });
} }
/// تنفيذ منطق التحميل الفعلي وتسجيل الدخول وتحديد وجهة الانتقال /// Initializes all heavy background services while the splash animation is running.
Future<void> _initializeBackgroundServices() async {
try {
await EncryptionHelper.initialize();
// --- [LAZY LOADING IN ACTION] ---
// This `Get.find()` call will create the NotificationController instance
// for the first time because it was defined with `lazyPut`.
final notificationController = Get.find<NotificationController>();
await notificationController.initNotifications();
// The same happens for FirebaseMessagesController.
final fcm = Get.find<FirebaseMessagesController>();
await fcm.requestFirebaseMessagingPermission();
_scheduleDailyNotifications(notificationController);
_initializeQuickActions();
} catch (e, st) {
CRUD.addError('background_init_error: $e', st.toString(), 'SplashScreen');
}
}
/// Determines the next screen based on user's login state.
Future<void> _performNavigationLogic() async { Future<void> _performNavigationLogic() async {
// تنفيذ المهام الأولية
await _getPackageInfo(); await _getPackageInfo();
SecureStorage().saveData('iss', 'mobile-app:'); SecureStorage().saveData('iss', 'mobile-app:');
// تحديد الشاشة التالية
if (box.read(BoxName.onBoarding) == null) { if (box.read(BoxName.onBoarding) == null) {
Get.off(() => OnBoardingPage()); Get.off(() => OnBoardingPage());
} else if (box.read(BoxName.email) != null && } else if (box.read(BoxName.email) != null &&
box.read(BoxName.phone) != null && box.read(BoxName.phone) != null &&
box.read(BoxName.isVerified) == '1') { box.read(BoxName.isVerified) == '1') {
// -- النقطة الأهم -- // `Get.find()` creates the LoginController instance here.
// هنا ننتظر انتهاء عملية تسجيل الدخول قبل الانتقال final loginController = Get.find<LoginController>();
await Get.put(LoginController()).loginUsingCredentials( // The loginController itself will handle navigation via Get.offAll() upon success.
await loginController.loginUsingCredentials(
box.read(BoxName.passengerID).toString(), box.read(BoxName.passengerID).toString(),
box.read(BoxName.email).toString(), box.read(BoxName.email).toString(),
); );
// بعد هذه العملية، سيتولى LoginController بنفسه الانتقال للصفحة الرئيسية أو صفحة الدخول
} else { } else {
Get.off(() => LoginPage()); Get.off(() => LoginPage());
} }
} }
/// جلب معلومات الحزمة لعرض إصدار التطبيق
Future<void> _getPackageInfo() async { Future<void> _getPackageInfo() async {
final info = await PackageInfo.fromPlatform(); try {
box.write(BoxName.packagInfo, info.version); final info = await PackageInfo.fromPlatform();
update(); box.write(BoxName.packagInfo, info.version);
update();
} catch (e) {
print("Could not get package info: $e");
}
}
void _scheduleDailyNotifications(NotificationController controller) {
try {
final List<String> msgs = passengerMessages ?? const [];
if (msgs.isEmpty) {
controller.scheduleNotificationsForSevenDays(
'Intaleq', 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.', "tone1");
} else {
final rnd = Random();
final raw = msgs[rnd.nextInt(msgs.length)];
final parts = raw.split(':');
final title = parts.isNotEmpty ? parts.first.trim() : 'Intaleq';
final body = parts.length > 1
? parts.sublist(1).join(':').trim()
: 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.';
controller.scheduleNotificationsForSevenDays(
title.isEmpty ? 'Intaleq' : title,
body.isEmpty ? 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.' : body,
"tone1");
}
} catch (e, st) {
CRUD.addError('notif_init: $e', st.toString(), 'SplashScreen');
}
}
void _initializeQuickActions() {
final QuickActions quickActions = QuickActions();
quickActions.initialize((String shortcutType) {
Get.toNamed('/$shortcutType');
});
quickActions.setShortcutItems(<ShortcutItem>[
ShortcutItem(
type: 'shareApp', localizedTitle: 'Share App'.tr, icon: 'icon_share'),
ShortcutItem(
type: 'wallet', localizedTitle: 'Wallet'.tr, icon: 'icon_wallet'),
ShortcutItem(
type: 'profile', localizedTitle: 'Profile'.tr, icon: 'icon_user'),
ShortcutItem(
type: 'contactSupport',
localizedTitle: 'Contact Support'.tr,
icon: 'icon_support'),
]);
} }
@override @override
@@ -144,40 +194,3 @@ class SplashScreenController extends GetxController
super.onClose(); super.onClose();
} }
} }
// يمكن الإبقاء على هذه الفئة لواجهة المستخدم المتعلقة بالأمان كما في الملف الأصلي
class SecurityPage extends StatelessWidget {
const SecurityPage({
super.key,
});
@override
Widget build(BuildContext context) {
return MyScafolld(
title: "security_warning".tr,
body: [
Center(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Text(
"security_message".tr,
style: AppStyle.headTitle2,
),
TextButton(
onPressed: () async {
// await SecurityHelper.clearAllData();
},
child: Text(
"security_warning".tr,
),
),
],
),
),
)
],
isleading: false);
}
}

View File

@@ -8,47 +8,65 @@ class MyTranslation extends Translations {
"SYP": "ل.س", "SYP": "ل.س",
"Order": "طلب", "Order": "طلب",
"OrderVIP": "طلب VIP", "OrderVIP": "طلب VIP",
"Hi ,I Arrive your site": "أنا وصلت لموقعك",
"Cancel Trip": "إلغاء الرحلة", "Cancel Trip": "إلغاء الرحلة",
"Passenger Cancel Trip": "الراكب لغى الرحلة", "Passenger Cancel Trip": "الراكب ألغى الرحلة",
"VIP Order": "طلب VIP", "VIP Order": "طلب VIP",
"The driver accepted your trip": "الكابتن وافق على رحلتك", 'Hi ,I Arrive your site': "مرحبًا، لقد وصلت إلى موقعك",
"The driver accepted your trip": "السائق قبل رحلتك",
"message From passenger": "رسالة من الراكب", "message From passenger": "رسالة من الراكب",
"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.":
"تم إلغاء الرحلة. ستتم إضافة تكلفة الرحلة إلى محفظتك.", "تم إلغاء الرحلة. سيتم إضافة تكلفة الرحلة إلى محفظتك.",
"token change": "تغيير الرمز", "token change": "تغيير الرمز",
"face detect": "التحقق من الوجه", "face detect": "كشف الوجه",
"Face Detection Result": "نتيجة التحقق من الوجه", "Face Detection Result": "نتيجة كشف الوجه",
"similar": "مطابق", "similar": "مشابه",
"not similar": "غير مطابق", "not similar": "غير مشابه",
"Hi ,I will go now": "مرحباً، أنا ذاهب الآن", "Hi ,I will go now": "مرحبًا، سأذهب الآن",
"Passenger come to you": "الراكب قادم إليك", "Passenger come to you": "الراكب قادم إليك",
"Call Income": "مكالمة واردة", "Call Income": "مكالمة واردة",
"Call Income from Passenger": "مكالمة واردة من الراكب", "Call Income from Passenger": "مكالمة واردة من الراكب",
"Criminal Document Required": "مطلوب وثيقة لا حكم عليه", "Criminal Document Required": "مطلوب وثيقة جنائية",
"You should have upload it .": "يجب عليك رفعها.", "You should have upload it .": "يجب عليك تحميلها.",
"Call End": "انتهاء المكالمة", "Call End": "انتهاء المكالمة",
"The order has been accepted by another driver.": "The order has been accepted by another driver.":
"تم قبول الطلب من قبل كابتن آخر.", "تم قبول الطلب من قبل سائق آخر.",
"The order Accepted by another Driver": "الطلب تم قبوله من كابتن آخر", "The order Accepted by another Driver":
"تم قبول الطلب من قبل سائق آخر",
"We regret to inform you that another driver has accepted this order.": "We regret to inform you that another driver has accepted this order.":
"نأسف لإبلاغك بأن كابتن آخر قد قبل هذا الطلب.", "نأسف لإعلامك بأن سائقًا آخر قد قبل هذا الطلب.",
"Driver Applied the Ride for You": "الكابتن قام بطلب الرحلة لك", "Driver Applied the Ride for You": "السائق قدم الطلب لك",
"Applied": "تم التقديم", "Applied": "تم التقديم",
"My Wallet": "محفظتي", 'Pay by Sham Cash': 'الدفع عبر شام كاش',
"Top up Balance": "تعبئة الرصيد", 'Pay with Debit Card': 'الدفع ببطاقة الخصم',
"Please go to Car Driver": "الرجاء التوجه إلى سيارة الكابتن", "Please go to Car Driver": "يرجى الذهاب إلى سائق السيارة",
"Ok I will go now.": "حسناً، أنا ذاهب الآن.", "Ok I will go now.": "حسنًا، سأذهب الآن.",
"Accepted Ride": "تم قبول الرحلة", "Accepted Ride": "تم قبول الرحلة",
"Driver Accepted the Ride for You": "الكابتن قبل رحلتك", "Driver Accepted the Ride for You": "السائق قبل الرحلة لك",
"Promo": "عرض", "Promo": "عرض ترويجي",
"Show latest promo": "عرض آخر العروض", "Show latest promo": "عرض أحدث عرض ترويجي",
"Trip Monitoring": "مراقبة الرحلة", "Trip Monitoring": "مراقبة الرحلة",
"Driver Is Going To Passenger": "الكابتن بالطريق إليك", "Driver Is Going To Passenger": "السائق في طريقه إليك",
"Please stay on the picked point.": "الرجاء البقاء في نقطة الانطلاق.", "Please stay on the picked point.":
"message From Driver": "رسالة من الكابتن", "يرجى البقاء في نقطة الالتقاط المحددة.",
"message From Driver": "رسالة من السائق",
"Trip is Begin": "بدأت الرحلة", "Trip is Begin": "بدأت الرحلة",
"Cancel Trip from driver": "إلغاء الرحلة من السائق",
"We will look for a new driver.\nPlease wait.":
"هنبحث عن سائق جديد.\nمن فضلك انتظر.",
"The driver canceled your ride.": "السائق ألغى رحلتك.",
"Driver Finish Trip": "السائق أنهى الرحلة",
"you will pay to Driver": "هتدفع للسائق",
"Dont forget your personal belongings.": "متنساش حاجاتك الشخصية.",
"Please make sure you have all your personal belongings and that any remaining fare, if applicable, has been added to your wallet before leaving. Thank you for choosing the Intaleq app":
"من فضلك تأكد إن معاك كل حاجاتك الشخصية وإن أي مبلغ متبقي، لو موجود، تم إضافته لمحفظتك قبل ما تمشي. شكرًا لاستخدامك تطبيق انطلق",
"Finish Monitor": "إنهاء المراقبة",
"Trip finished": "الرحلة انتهت",
"Call Income from Driver": "مكالمة واردة من السائق",
"Driver Cancelled Your Trip": "السائق ألغى رحلتك",
"you will pay to Driver you will be pay the cost of driver time look to your Intaleq Wallet":
"هتدفع للسائق هتدفع تكلفة وقت السائق شوف محفظة Intaleq بتاعتك",
"Order Applied": "تم تطبيق الطلب",
"welcome to intaleq": "أهلاً بك في انطلق", "welcome to intaleq": "أهلاً بك في انطلق",
"login or register subtitle": "login or register subtitle":
"أدخل رقم موبايلك لتسجيل الدخول أو لإنشاء حساب جديد", "أدخل رقم موبايلك لتسجيل الدخول أو لإنشاء حساب جديد",
@@ -166,7 +184,10 @@ class MyTranslation extends Translations {
"Add funds using our secure methods": "Add funds using our secure methods":
"أضف رصيداً باستخدام طرقنا الآمنة", "أضف رصيداً باستخدام طرقنا الآمنة",
"Speed": "سرعة", "Speed": "سرعة",
"Comfort": "راحة", "Comfort": "‏مريحة",
"Intaleq Balance": "رصيد انطلق",
'Search for a starting point': 'ابحث عن نقطة انطلاق',
'Top up Balance to continue': 'اشحن الرصيد للمتابعة',
"Electric": "كهربائية", "Electric": "كهربائية",
"Lady": "سيدة", "Lady": "سيدة",
"Van": "عائلية", "Van": "عائلية",

View File

@@ -1,9 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'package:Intaleq/app_bindings.dart';
import 'package:Intaleq/controller/functions/crud.dart'; import 'package:Intaleq/controller/functions/crud.dart';
import 'package:Intaleq/controller/payment/paymob/paymob_response.dart';
import 'package:Intaleq/views/home/HomePage/contact_us.dart'; import 'package:Intaleq/views/home/HomePage/contact_us.dart';
import 'package:Intaleq/views/home/HomePage/share_app_page.dart'; import 'package:Intaleq/views/home/HomePage/share_app_page.dart';
import 'package:Intaleq/views/home/my_wallet/passenger_wallet.dart'; import 'package:Intaleq/views/home/my_wallet/passenger_wallet.dart';
@@ -16,287 +14,73 @@ import 'package:flutter_stripe/flutter_stripe.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:quick_actions/quick_actions.dart';
import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart';
import 'constant/api_key.dart'; import 'constant/api_key.dart';
import 'constant/info.dart'; import 'constant/info.dart';
import 'constant/notification.dart';
import 'controller/firebase/firbase_messge.dart';
import 'controller/firebase/local_notification.dart';
import 'controller/functions/encrypt_decrypt.dart';
import 'controller/functions/secure_storage.dart';
import 'controller/home/deep_link_controller.dart';
import 'controller/local/local_controller.dart'; import 'controller/local/local_controller.dart';
import 'controller/local/translations.dart'; import 'controller/local/translations.dart';
import 'controller/payment/paymob/paymob_wallet.dart';
import 'firebase_options.dart'; import 'firebase_options.dart';
import 'models/db_sql.dart'; import 'models/db_sql.dart';
import 'splash_screen_page.dart'; import 'splash_screen_page.dart';
// -- Global instances for easy access --
final box = GetStorage(); final box = GetStorage();
final storage = FlutterSecureStorage(); final storage = FlutterSecureStorage();
// final PaymobPayment paymobPayment = PaymobPayment();
// final PaymobPayment paymobPayment = PaymobPayment();
// final PaymobPaymentWallet paymobPaymentWallet = PaymobPaymentWallet();
DbSql sql = DbSql.instance; DbSql sql = DbSql.instance;
// EncryptionHelper encryptionHelper = EncryptionHelper.instance;
// Firebase background message handler must be a top-level function.
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> backgroundMessageHandler(RemoteMessage message) async { Future<void> backgroundMessageHandler(RemoteMessage message) async {
await Firebase.initializeApp(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
print("Handling a background message: ${message.messageId}");
FirebaseMessagesController().fireBaseTitles(message);
} }
void main() { void main() {
// ⚠️ لا تستدعِ ensureInitialized هنا خارج الزون // Use runZonedGuarded to catch all unhandled exceptions in the app.
runZonedGuarded<Future<void>>(() async {
runZonedGuarded(() async { // --- Step 1: Critical initializations before runApp() ---
// 1) أنشئ الـ Binding في نفس الـ Zone // These must complete before the UI can be built.
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// 2) تهيئات خفيفة فقط قبل runApp
await GetStorage.init(); await GetStorage.init();
WakelockPlus.enable();
if (Platform.isAndroid || Platform.isIOS) { if (Platform.isAndroid || Platform.isIOS) {
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform); options: DefaultFirebaseOptions.currentPlatform);
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler); FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
} }
// Stripe key initialization is very fast.
Stripe.publishableKey = AK.publishableKey; Stripe.publishableKey = AK.publishableKey;
Get.put(DeepLinkController(), permanent: true);
// 3) شغّل التطبيق فوراً // Lock screen orientation.
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
Get.put(LocaleController());
// --- Step 2: Run the app immediately ---
// All heavy initializations are deferred to the SplashScreen.
runApp(const MyApp()); runApp(const MyApp());
// 4) بعد أول إطار: التهيئات الثقيلة
WidgetsBinding.instance.addPostFrameCallback((_) async {
// لا Wakelock عالمي — فعّله فقط أثناء الرحلة
await WakelockPlus.enable();
final AppInitializer initializer = AppInitializer();
await initializer.initializeApp();
await EncryptionHelper.initialize();
final notificationController = Get.put(NotificationController());
await notificationController.initNotifications();
// أجّل طلب صلاحيات/توكن FCM
final fcm = FirebaseMessagesController();
await fcm.requestFirebaseMessagingPermission();
await fcm.getNotificationSettings();
await fcm.getToken();
// Generate a random index to pick a message
// Generate and schedule passenger daily notifications safely
try {
// 1) تأكد أن القائمة موجودة وغير فارغة
final List<String> msgs = passengerMessages ?? const [];
if (msgs.isEmpty) {
// fallback افتراضي إذا ما في رسائل
const fallbackTitle = 'Intaleq';
const fallbackBody = 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.';
notificationController.scheduleNotificationsForSevenDays(
fallbackTitle,
fallbackBody,
"tone1",
);
} else {
// 2) اختَر عنصرًا عشوائيًا بأمان
final rnd = Random();
final idx = rnd.nextInt(msgs.length); // msgs.length > 0 أكيد
final raw = msgs[idx];
// 3) افصل العنوان/النص بأمان حتى لو ما في ':'
final parts = raw.split(':');
final title = parts.isNotEmpty ? parts.first.trim() : 'Intaleq';
final body = parts.length > 1
? parts.sublist(1).join(':').trim() // يحافظ على أي ':' إضافية
: 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.';
// 4) جدولة الإشعارات
notificationController.scheduleNotificationsForSevenDays(
title.isEmpty ? 'Intaleq' : title,
body.isEmpty ? 'مرحباً بك! تابع رحلاتك بأمان مع انطلق.' : body,
"tone1",
);
}
} catch (e, st) {
// لا تعطل التشغيل بسبب إشعار اختياري
CRUD.addError('notif_init: $e', st.toString(), 'main');
}
final QuickActions quickActions = QuickActions();
quickActions.initialize((String shortcutType) {
// print('Activated shortcut: $shortcutType');
if (shortcutType == 'share_app') {
Get.toNamed('/shareApp');
} else if (shortcutType == 'wallet') {
Get.toNamed('/wallet');
} else if (shortcutType == 'profile') {
Get.toNamed('/profile');
} else if (shortcutType == 'contact_support') {
Get.toNamed('/contactSupport');
}
});
quickActions.setShortcutItems(<ShortcutItem>[
ShortcutItem(
type: 'share_app',
localizedTitle: 'Share App'.tr,
icon: 'icon_share',
),
ShortcutItem(
type: 'wallet',
localizedTitle: 'Wallet'.tr,
icon: 'icon_wallet',
),
ShortcutItem(
type: 'profile',
localizedTitle: 'Profile'.tr,
icon: 'icon_user',
),
ShortcutItem(
type: 'contact_support',
localizedTitle: 'Contact Support'.tr,
icon: 'icon_support',
),
]);
});
}, (error, stack) { }, (error, stack) {
// بديل آمن للتجميع // Global error handler.
final s = error.toString(); final s = error.toString();
final ignored = s.contains('PERMISSION_DENIED') || final ignored = s.contains('PERMISSION_DENIED') ||
s.contains('FormatException') || s.contains('FormatException') ||
s.contains('Null check operator used on a null value'); s.contains('Null check operator used on a null value');
if (!ignored) CRUD.addError(s, stack.toString(), 'main');
if (!ignored) {
CRUD.addError(s, stack.toString(), 'main_zone_guard');
}
}); });
} // void main() async { }
// WidgetsFlutterBinding.ensureInitialized();
// WakelockPlus.enable();
// await GetStorage.init();
// // --- إضافة جديدة: تهيئة وحدة التحكم في الروابط العميقة ---
// Get.put(DeepLinkController(), permanent: true);
// // ----------------------------------------------------
// final AppInitializer initializer = AppInitializer();
// await initializer.initializeApp();
// await EncryptionHelper.initialize();
// NotificationController notificationController =
// Get.put(NotificationController());
// // Stripe.publishableKey = AK.publishableKey;
// // if (box.read(BoxName.driverID) != null) {}
// if (Platform.isAndroid || Platform.isIOS) {
// await Firebase.initializeApp(
// options: DefaultFirebaseOptions.currentPlatform,
// );
// await FirebaseMessagesController().requestFirebaseMessagingPermission();
// FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
// List<Future> initializationTasks = [
// FirebaseMessagesController().getNotificationSettings(),
// FirebaseMessagesController().getToken(),
// ];
// // cameras = await availableCameras();
// await Future.wait(initializationTasks);
// SystemChrome.setPreferredOrientations([
// DeviceOrientation.portraitUp,
// DeviceOrientation.portraitDown,
// ]);
// }
// await notificationController.initNotifications();
// // Generate a random index to pick a message
// final random = Random();
// final randomMessage = messages[random.nextInt(messages.length)];
// // Schedule the notification with the random message
// notificationController.scheduleNotificationsForSevenDays(
// randomMessage.split(':')[0],
// randomMessage.split(':')[1],
// "tone1",
// );
// final QuickActions quickActions = QuickActions();
// quickActions.initialize((String shortcutType) {
// // print('Activated shortcut: $shortcutType');
// if (shortcutType == 'share_app') {
// Get.toNamed('/shareApp');
// } else if (shortcutType == 'wallet') {
// Get.toNamed('/wallet');
// } else if (shortcutType == 'profile') {
// Get.toNamed('/profile');
// } else if (shortcutType == 'contact_support') {
// Get.toNamed('/contactSupport');
// }
// });
// quickActions.setShortcutItems(<ShortcutItem>[
// ShortcutItem(
// type: 'share_app',
// localizedTitle: 'Share App'.tr,
// icon: 'icon_share',
// ),
// ShortcutItem(
// type: 'wallet',
// localizedTitle: 'Wallet'.tr,
// icon: 'icon_wallet',
// ),
// ShortcutItem(
// type: 'profile',
// localizedTitle: 'Profile'.tr,
// icon: 'icon_user',
// ),
// ShortcutItem(
// type: 'contact_support',
// localizedTitle: 'Contact Support'.tr,
// icon: 'icon_support',
// ),
// ]);
// runZonedGuarded<Future<void>>(() async {
// runApp(const MyApp());
// }, (error, stack) {
// // ==== START: ERROR FILTER ====
// String errorString = error.toString();
// // Print all errors to the local debug console for development
// print("Caught Dart error: $error");
// print(stack);
// // We will check if the error contains keywords for errors we want to ignore.
// // If it's one of them, we will NOT send it to the server.
// bool isIgnoredError = errorString.contains('PERMISSION_DENIED') ||
// errorString.contains('FormatException') ||
// errorString.contains('Null check operator used on a null value');
// if (!isIgnoredError) {
// // Only send the error to the server if it's not in our ignore list.
// CRUD.addError(error.toString(), stack.toString(), 'main');
// } else {
// print("Ignoring error and not sending to server: $errorString");
// }
// // ==== END: ERROR FILTER ====
// });
// }
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
// This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
LocaleController localController = Get.put(LocaleController()); // Get.find() is used here because LocaleController is already put in AppBindings.
final LocaleController localController = Get.find<LocaleController>();
return GetMaterialApp( return GetMaterialApp(
title: AppInformation.appName, title: AppInformation.appName,
@@ -304,19 +88,21 @@ class MyApp extends StatelessWidget {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
locale: localController.language, locale: localController.language,
theme: localController.appTheme, theme: localController.appTheme,
key: UniqueKey(),
// routes: {'/':const HomePage()}, // --- [CRITICAL] ---
// home: LoginCaptin()); // initialBinding tells GetX to run AppBindings once at the start.
// This sets up all our controllers (put, lazyPut) for the entire app lifecycle.
initialBinding: AppBindings(),
initialRoute: '/', initialRoute: '/',
getPages: [ getPages: [
GetPage(name: '/', page: () => SplashScreen()), GetPage(name: '/', page: () => const SplashScreen()),
// These routes are used by QuickActions and other navigation events.
GetPage(name: '/shareApp', page: () => ShareAppPage()), GetPage(name: '/shareApp', page: () => ShareAppPage()),
GetPage(name: '/wallet', page: () => PassengerWallet()), GetPage(name: '/wallet', page: () => const PassengerWallet()),
GetPage(name: '/profile', page: () => PassengerProfilePage()), GetPage(name: '/profile', page: () => PassengerProfilePage()),
GetPage(name: '/contactSupport', page: () => ContactUsPage()), GetPage(name: '/contactSupport', page: () => ContactUsPage()),
], ],
// home: SplashScreen()
); );
} }
} }

View File

@@ -26,7 +26,7 @@ class CarType {
{required this.carType, required this.carDetail, required this.image}); {required this.carType, required this.carDetail, required this.image});
} }
// --- List of Car Types with NEW order and 'Electric' car --- // --- List of Car Types (unchanged) ---
List<CarType> carTypes = [ List<CarType> carTypes = [
CarType( CarType(
carType: 'Speed', carType: 'Speed',
@@ -49,10 +49,6 @@ List<CarType> carTypes = [
carType: 'Van', carType: 'Van',
carDetail: 'Van for familly'.tr, carDetail: 'Van for familly'.tr,
image: 'assets/images/bus.png'), image: 'assets/images/bus.png'),
// CarType(
// carType: 'Scooter',
// carDetail: 'Delivery service'.tr,
// image: 'assets/images/moto.png'),
CarType( CarType(
carType: 'Rayeh Gai', carType: 'Rayeh Gai',
carDetail: "Best choice for cities".tr, carDetail: "Best choice for cities".tr,
@@ -65,6 +61,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
final textToSpeechController = Get.put(TextToSpeechController()); final textToSpeechController = Get.put(TextToSpeechController());
void _prepareCarTypes(MapPassengerController controller) { void _prepareCarTypes(MapPassengerController controller) {
// This logic remains the same
if (controller.distance > 33) { if (controller.distance > 33) {
if (!carTypes.any((car) => car.carType == 'Rayeh Gai')) { if (!carTypes.any((car) => car.carType == 'Rayeh Gai')) {
carTypes.add(CarType( carTypes.add(CarType(
@@ -87,56 +84,69 @@ class CarDetailsTypeToChoose extends StatelessWidget {
controller.rideConfirm == false)) { controller.rideConfirm == false)) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
// Added a BackdropFilter for a modern glassmorphism effect
return Positioned( return Positioned(
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
child: Container( child: ClipRRect(
decoration: BoxDecoration( borderRadius: const BorderRadius.only(
color: AppColor.secondaryColor, topLeft: Radius.circular(30),
borderRadius: const BorderRadius.only( topRight: Radius.circular(30),
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 20,
spreadRadius: 5,
),
],
), ),
child: Column( child: BackdropFilter(
mainAxisSize: MainAxisSize.min, filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
children: [ child: Container(
_buildHeader(controller), decoration: BoxDecoration(
const Divider(height: 1), color: AppColor.secondaryColor.withOpacity(0.9),
_buildNegativeBalanceWarning(controller), borderRadius: const BorderRadius.only(
SizedBox( topLeft: Radius.circular(30),
height: 130, topRight: Radius.circular(30),
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: carTypes.length,
itemBuilder: (context, index) {
final carType = carTypes[index];
final isSelected = controller.selectedIndex == index;
return _buildHorizontalCarCard(
context, controller, carType, isSelected, index);
},
), ),
border: Border.all(color: AppColor.writeColor.withOpacity(0.1)),
), ),
_buildPromoButton(context, controller), child: Column(
], mainAxisSize: MainAxisSize.min,
children: [
// Added a small handle for visual cue
Container(
width: 40,
height: 5,
margin: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: AppColor.writeColor.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
),
_buildHeader(controller),
_buildNegativeBalanceWarning(controller),
SizedBox(
height: 140, // Increased height for better spacing
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 12),
itemCount: carTypes.length,
itemBuilder: (context, index) {
final carType = carTypes[index];
final isSelected = controller.selectedIndex == index;
return _buildHorizontalCarCard(
context, controller, carType, isSelected, index);
},
),
),
_buildPromoButton(context, controller),
const SizedBox(height: 8), // Added padding at the bottom
],
),
),
), ),
), ),
); );
}); });
} }
// --- All other methods are here, with updates for 'Electric' car --- // --- All other methods are here, with updated designs ---
Widget _buildPromoButton( Widget _buildPromoButton(
BuildContext context, MapPassengerController controller) { BuildContext context, MapPassengerController controller) {
@@ -145,32 +155,34 @@ class CarDetailsTypeToChoose extends StatelessWidget {
} }
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
child: OutlinedButton( child: GestureDetector(
onPressed: () => _showPromoCodeDialog(context, controller), onTap: () => _showPromoCodeDialog(context, controller),
style: OutlinedButton.styleFrom( child: Container(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
backgroundColor: AppColor.primaryColor.withOpacity(0.05), decoration: BoxDecoration(
side: BorderSide( color: AppColor.primaryColor.withOpacity(0.1),
color: AppColor.primaryColor.withOpacity(0.7), borderRadius: BorderRadius.circular(16),
width: 1.5, border: Border.all(
), color: AppColor.primaryColor.withOpacity(0.5),
shape: RoundedRectangleBorder( width: 1,
borderRadius: BorderRadius.circular(12),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.local_offer_outlined,
color: AppColor.primaryColor, size: 20),
const SizedBox(width: 8),
Text(
'Have a promo code?'.tr,
style: AppStyle.title
.copyWith(fontSize: 16, color: AppColor.primaryColor),
), ),
], ),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.local_offer_outlined,
color: AppColor.primaryColor, size: 22),
const SizedBox(width: 10),
Text(
'Have a promo code?'.tr,
style: AppStyle.title.copyWith(
fontSize: 16,
color: AppColor.primaryColor,
fontWeight: FontWeight.w600),
),
],
),
), ),
), ),
); );
@@ -263,35 +275,55 @@ class CarDetailsTypeToChoose extends StatelessWidget {
_showCarDetailsDialog( _showCarDetailsDialog(
context, controller, carType, textToSpeechController); context, controller, carType, textToSpeechController);
}, },
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(20),
child: AnimatedContainer( child: AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
width: 110, width: 120, // Increased width
margin: const EdgeInsets.only(right: 12), margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.all(8), // Added padding
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected gradient: LinearGradient(
? AppColor.primaryColor.withOpacity(0.15) begin: Alignment.topLeft,
: AppColor.writeColor.withOpacity(0.05), end: Alignment.bottomRight,
borderRadius: BorderRadius.circular(16), colors: isSelected
border: Border.all( ? [
color: isSelected ? AppColor.primaryColor : Colors.transparent, AppColor.primaryColor.withOpacity(0.3),
width: 2.0, AppColor.primaryColor.withOpacity(0.1)
]
: [
AppColor.writeColor.withOpacity(0.05),
AppColor.writeColor.withOpacity(0.1)
],
), ),
borderRadius: BorderRadius.circular(20), // More rounded corners
border: Border.all(
color: isSelected
? AppColor.primaryColor
: AppColor.writeColor.withOpacity(0.2),
width: isSelected ? 2.5 : 1.0,
),
boxShadow: isSelected
? [
BoxShadow(
color: AppColor.primaryColor.withOpacity(0.3),
blurRadius: 10,
spreadRadius: 1,
)
]
: [],
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceAround, // Better alignment
children: [ children: [
Image.asset(carType.image, height: 50), Image.asset(carType.image, height: 55), // Slightly larger image
const SizedBox(height: 8),
Text( Text(
carType.carType.tr, carType.carType.tr,
style: AppStyle.subtitle style: AppStyle.subtitle
.copyWith(fontWeight: FontWeight.bold, fontSize: 14), .copyWith(fontWeight: FontWeight.bold, fontSize: 15),
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 4),
_buildPriceDisplay(controller, carType), _buildPriceDisplay(controller, carType),
], ],
), ),
@@ -301,20 +333,44 @@ class CarDetailsTypeToChoose extends StatelessWidget {
Widget _buildHeader(MapPassengerController controller) { Widget _buildHeader(MapPassengerController controller) {
return Padding( return Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 12), padding: const EdgeInsets.fromLTRB(20, 8, 20, 16),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Choose your ride'.tr, Text('Choose your ride'.tr,
style: AppStyle.headTitle.copyWith(fontSize: 22)), style: AppStyle.headTitle.copyWith(fontSize: 24)),
const SizedBox(height: 4), const SizedBox(height: 8),
Text( // Added icons for better visual representation
'${controller.distance} ${'KM'.tr}${controller.hours > 0 ? '${controller.hours}h ${controller.minutes}m' : '${controller.minutes} min'}', Row(
style: AppStyle.subtitle.copyWith( children: [
color: AppColor.primaryColor, fontWeight: FontWeight.bold), Icon(Icons.map_outlined,
color: AppColor.primaryColor, size: 16),
const SizedBox(width: 4),
Text(
'${controller.distance} ${'KM'.tr}',
style: AppStyle.subtitle.copyWith(
color: AppColor.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14),
),
const SizedBox(width: 12),
Icon(Icons.timer_outlined,
color: AppColor.primaryColor, size: 16),
const SizedBox(width: 4),
Text(
controller.hours > 0
? '${controller.hours}h ${controller.minutes}m'
: '${controller.minutes} min',
style: AppStyle.subtitle.copyWith(
color: AppColor.primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14),
),
],
), ),
], ],
), ),
@@ -328,8 +384,12 @@ class CarDetailsTypeToChoose extends StatelessWidget {
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) {
return Container( return Container(
color: AppColor.redColor.withOpacity(0.8), margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: AppColor.redColor.withOpacity(0.9),
borderRadius: BorderRadius.circular(12),
),
child: Row( child: Row(
children: [ children: [
const Icon(Icons.error_outline, color: Colors.white, size: 24), const Icon(Icons.error_outline, color: Colors.white, size: 24),
@@ -337,7 +397,8 @@ class CarDetailsTypeToChoose extends StatelessWidget {
Expanded( Expanded(
child: Text( child: Text(
'${'You have a negative balance of'.tr} ${passengerWallet.toStringAsFixed(2)} ${'SYP'.tr}.', '${'You have a negative balance of'.tr} ${passengerWallet.toStringAsFixed(2)} ${'SYP'.tr}.',
style: AppStyle.subtitle.copyWith(color: Colors.white))), style: AppStyle.subtitle.copyWith(
color: Colors.white, fontWeight: FontWeight.w600))),
], ],
), ),
); );
@@ -350,12 +411,13 @@ class CarDetailsTypeToChoose extends StatelessWidget {
return Text( return Text(
'${_getPassengerPriceText(carType, mapPassengerController)} ${'SYP'.tr}', '${_getPassengerPriceText(carType, mapPassengerController)} ${'SYP'.tr}',
style: AppStyle.subtitle.copyWith( style: AppStyle.subtitle.copyWith(
fontSize: 14, fontSize: 15, // Increased font size
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColor.primaryColor)); color: AppColor.primaryColor));
} }
// UPDATED to include 'Electric' // --- LOGIC METHODS (UNCHANGED) ---
String _getPassengerPriceText( String _getPassengerPriceText(
CarType carType, MapPassengerController mapPassengerController) { CarType carType, MapPassengerController mapPassengerController) {
switch (carType.carType) { switch (carType.carType) {
@@ -391,79 +453,97 @@ class CarDetailsTypeToChoose extends StatelessWidget {
Dialog( Dialog(
shape: shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(24.0)), RoundedRectangleBorder(borderRadius: BorderRadius.circular(24.0)),
backgroundColor: AppColor.secondaryColor, backgroundColor:
child: Padding( Colors.transparent, // Make dialog background transparent
padding: const EdgeInsets.fromLTRB(20, 24, 20, 20), child: Stack(
child: Column( clipBehavior: Clip.none,
mainAxisSize: MainAxisSize.min, alignment: Alignment.topCenter,
children: [ children: [
Image.asset(carType.image, height: 70), // Main content container
const SizedBox(height: 16), Container(
Row( margin:
mainAxisAlignment: MainAxisAlignment.center, const EdgeInsets.only(top: 50), // Make space for the image
crossAxisAlignment: CrossAxisAlignment.center, padding: const EdgeInsets.fromLTRB(20, 70, 20, 20),
children: [ decoration: BoxDecoration(
Text( color: AppColor.secondaryColor,
carType.carType.tr, borderRadius: BorderRadius.circular(24.0),
style: AppStyle.headTitle.copyWith(fontSize: 22),
),
const SizedBox(width: 8),
IconButton(
onPressed: () => textToSpeechController.speakText(
_getCarDescription(mapPassengerController, carType)),
icon: Icon(Icons.volume_up_outlined,
color: AppColor.primaryColor, size: 24),
),
],
), ),
const SizedBox(height: 8), child: Column(
Text( mainAxisSize: MainAxisSize.min,
_getCarDescription(mapPassengerController, carType),
textAlign: TextAlign.center,
style: AppStyle.subtitle.copyWith(
color: AppColor.writeColor.withOpacity(0.7),
fontSize: 15,
height: 1.4,
),
),
const SizedBox(height: 24),
Row(
children: [ children: [
Expanded( Row(
child: TextButton( mainAxisAlignment: MainAxisAlignment.center,
onPressed: () => Get.back(), crossAxisAlignment: CrossAxisAlignment.center,
style: TextButton.styleFrom( children: [
foregroundColor: AppColor.writeColor.withOpacity(0.8), Text(
padding: const EdgeInsets.symmetric(vertical: 14), carType.carType.tr,
shape: RoundedRectangleBorder( style: AppStyle.headTitle.copyWith(fontSize: 24),
borderRadius: BorderRadius.circular(12)),
), ),
child: Text('Cancel'.tr), const SizedBox(width: 8),
IconButton(
onPressed: () => textToSpeechController.speakText(
_getCarDescription(
mapPassengerController, carType)),
icon: Icon(Icons.volume_up_outlined,
color: AppColor.primaryColor, size: 26),
),
],
),
const SizedBox(height: 12),
Text(
_getCarDescription(mapPassengerController, carType),
textAlign: TextAlign.center,
style: AppStyle.subtitle.copyWith(
color: AppColor.writeColor.withOpacity(0.7),
fontSize: 16,
height: 1.5,
), ),
), ),
const SizedBox(width: 12), const SizedBox(height: 28),
Expanded( Row(
child: MyElevatedButton( children: [
kolor: AppColor.greenColor, Expanded(
title: 'Next'.tr, child: TextButton(
onPressed: () { onPressed: () => Get.back(),
Get.back(); style: TextButton.styleFrom(
_handleCarSelection( foregroundColor:
context, mapPassengerController, carType); AppColor.writeColor.withOpacity(0.8),
}, padding: const EdgeInsets.symmetric(vertical: 14),
), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: Text('Cancel'.tr),
),
),
const SizedBox(width: 12),
Expanded(
child: MyElevatedButton(
kolor: AppColor.greenColor,
title: 'Next'.tr,
onPressed: () {
Get.back();
_handleCarSelection(
context, mapPassengerController, carType);
},
),
),
],
), ),
], ],
), ),
], ),
), // Positioned car image
Positioned(
top: -10,
child: Image.asset(carType.image, height: 120),
),
],
), ),
), ),
barrierColor: Colors.black.withOpacity(0.5), barrierColor: Colors.black.withOpacity(0.6),
); );
} }
// UPDATED to include 'Electric'
String _getCarDescription( String _getCarDescription(
MapPassengerController mapPassengerController, CarType carType) { MapPassengerController mapPassengerController, CarType carType) {
switch (carType.carType) { switch (carType.carType) {
@@ -553,7 +633,6 @@ class CarDetailsTypeToChoose extends StatelessWidget {
} }
} }
// UPDATED to include 'Electric'
double _getOriginalPrice( double _getOriginalPrice(
CarType carType, MapPassengerController mapPassengerController) { CarType carType, MapPassengerController mapPassengerController) {
switch (carType.carType) { switch (carType.carType) {
@@ -595,6 +674,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
} }
} }
// --- BurcMoney Widget (Unchanged) ---
class BurcMoney extends StatelessWidget { class BurcMoney extends StatelessWidget {
const BurcMoney({super.key}); const BurcMoney({super.key});

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
@@ -11,123 +13,232 @@ import '../../../controller/functions/toast.dart';
import '../../../controller/home/map_passenger_controller.dart'; import '../../../controller/home/map_passenger_controller.dart';
import '../../../main.dart'; import '../../../main.dart';
// ---------------------------------------------------
// -- Widget for Destination Point Search (Optimized) --
// ---------------------------------------------------
/// A more optimized and cleaner implementation of the destination search form.
///
/// Improvements:
/// 1. **Widget Refactoring**: The UI is broken down into smaller, focused widgets
/// (_SearchField, _QuickActions, _SearchResults) to prevent unnecessary rebuilds.
/// 2. **State Management Scoping**: `GetBuilder` is used only on widgets that
/// actually need to update, not the entire form.
/// 3. **Reduced Build Logic**: Logic like reading from `box` is done once.
/// 4. **Readability**: Code is cleaner and easier to follow.
GetBuilder<MapPassengerController> formSearchPlacesDestenation() { GetBuilder<MapPassengerController> formSearchPlacesDestenation() {
if (box.read(BoxName.addWork).toString() == '' || // --- [تحسين] قراءة القيم مرة واحدة في بداية البناء ---
box.read(BoxName.addHome).toString() == '') { // Store box values in local variables to avoid repeated calls inside the build method.
final String addWorkValue =
box.read(BoxName.addWork)?.toString() ?? 'addWork';
final String addHomeValue =
box.read(BoxName.addHome)?.toString() ?? 'addHome';
// --- [ملاحظة] تأكد من أن القيم الأولية موجودة ---
// This initialization can be moved to your app's startup logic or a splash screen controller.
if (addWorkValue.isEmpty || addHomeValue.isEmpty) {
box.write(BoxName.addWork, 'addWork'); box.write(BoxName.addWork, 'addWork');
box.write(BoxName.addHome, 'addHome'); box.write(BoxName.addHome, 'addHome');
} }
return GetBuilder<MapPassengerController>( return GetBuilder<MapPassengerController>(
builder: (controller) => Column( id: 'destination_form', // Use an ID to allow targeted updates
children: [ builder: (controller) {
Padding( return Column(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), children: [
child: Row( // --- Widget for the search text field ---
children: [ _SearchField(controller: controller),
Expanded(
child: TextFormField( // --- Widget for "Add Work" and "Add Home" buttons ---
controller: controller.placeDestinationController, _QuickActions(
onChanged: (value) { controller: controller,
if (controller.placeDestinationController.text.length > 2) { addWorkValue: addWorkValue,
controller.getPlaces(); addHomeValue: addHomeValue,
controller.changeHeightPlaces(); ),
} else if (controller
.placeDestinationController.text.isEmpty) { // --- Widget for displaying search results, wrapped in its own GetBuilder ---
controller.clearPlacesDestination(); _SearchResults(),
controller.changeHeightPlaces(); ],
} );
}, },
decoration: InputDecoration( );
hintText: controller.hintTextDestinationPoint, }
hintStyle:
AppStyle.subtitle.copyWith(color: Colors.grey[600]), // ---------------------------------------------------
prefixIcon: // -- Private Helper Widgets for Cleaner Code --
const Icon(Icons.search, color: AppColor.primaryColor), // ---------------------------------------------------
suffixIcon: controller
.placeDestinationController.text.isNotEmpty /// A dedicated widget for the search input field.
? IconButton( class _SearchField extends StatefulWidget {
icon: Icon(Icons.clear, color: Colors.grey[400]), final MapPassengerController controller;
onPressed: () {
controller.placeDestinationController.clear(); const _SearchField({required this.controller});
controller.clearPlacesDestination();
controller.changeHeightPlaces(); @override
}, State<_SearchField> createState() => _SearchFieldState();
) }
: null,
contentPadding: const EdgeInsets.symmetric( class _SearchFieldState extends State<_SearchField> {
horizontal: 16.0, vertical: 10.0), Timer? _debounce;
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0), // --- [إصلاح] Listener لتحديث الواجهة عند تغيير النص لإظهار/إخفاء زر المسح ---
borderSide: BorderSide.none, void _onTextChanged() {
), if (mounted) {
focusedBorder: OutlineInputBorder( setState(() {});
borderRadius: BorderRadius.circular(8.0), }
borderSide: BorderSide(color: AppColor.primaryColor), }
),
filled: true, @override
fillColor: Colors.grey[50], void initState() {
), super.initState();
// Add listener to update the suffix icon when text changes
widget.controller.placeDestinationController.addListener(_onTextChanged);
}
// --- [تحسين] إضافة Debouncer لتأخير البحث أثناء الكتابة ---
void _onSearchChanged(String query) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
if (query.length > 2) {
widget.controller.getPlaces();
widget.controller.changeHeightPlaces();
} else if (query.isEmpty) {
widget.controller.clearPlacesDestination();
widget.controller.changeHeightPlaces();
}
});
}
@override
void dispose() {
_debounce?.cancel();
// Remove the listener to prevent memory leaks
widget.controller.placeDestinationController.removeListener(_onTextChanged);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
children: [
Expanded(
child: TextFormField(
controller: widget.controller.placeDestinationController,
onChanged: _onSearchChanged,
decoration: InputDecoration(
hintText: widget.controller.hintTextDestinationPoint,
hintStyle: AppStyle.subtitle.copyWith(color: Colors.grey[600]),
prefixIcon:
const Icon(Icons.search, color: AppColor.primaryColor),
// --- [إصلاح] تم استبدال Obx بشرط بسيط لأن `setState` يعيد بناء الواجهة الآن ---
suffixIcon: widget
.controller.placeDestinationController.text.isNotEmpty
? IconButton(
icon: Icon(Icons.clear, color: Colors.grey[400]),
onPressed: () {
widget.controller.placeDestinationController.clear();
// The listener will automatically handle the UI update
// And _onSearchChanged will handle clearing the results
},
)
: null, // Use null instead of SizedBox for better practice
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 10.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide.none,
), ),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(color: AppColor.primaryColor),
),
filled: true,
fillColor: Colors.grey[50],
), ),
const SizedBox(width: 8.0), ),
IconButton(
onPressed: () {
controller.changeMainBottomMenuMap();
controller.changePickerShown();
},
icon: Icon(Icons.location_on_outlined,
color: AppColor.accentColor, size: 30),
tooltip: controller.isAnotherOreder
? 'Pick destination on map'
: 'Pick on map',
),
],
), ),
), const SizedBox(width: 8.0),
Padding( IconButton(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), onPressed: () {
child: Row( widget.controller.changeMainBottomMenuMap();
mainAxisAlignment: MainAxisAlignment.spaceAround, widget.controller.changePickerShown();
children: [ },
_buildQuickActionButton( icon: Icon(Icons.location_on_outlined,
icon: Icons.work_outline, color: AppColor.accentColor, size: 30),
text: box.read(BoxName.addWork) == 'addWork' tooltip: widget.controller.isAnotherOreder
? 'Add Work'.tr ? 'Pick destination on map'
: 'To Work'.tr, : 'Pick on map',
onTap: () async {
if (box.read(BoxName.addWork) == 'addWork') {
controller.workLocationFromMap = true;
controller.changeMainBottomMenuMap();
controller.changePickerShown();
} else {
_handleQuickAction(controller, BoxName.addWork, 'To Work');
}
},
onLongPress: () =>
_showChangeLocationDialog(controller, 'Work'),
),
_buildQuickActionButton(
icon: Icons.home_outlined,
text: box.read(BoxName.addHome) == 'addHome'
? 'Add Home'.tr
: 'To Home'.tr,
onTap: () async {
if (box.read(BoxName.addHome) == 'addHome') {
controller.homeLocationFromMap = true;
controller.changeMainBottomMenuMap();
controller.changePickerShown();
} else {
_handleQuickAction(controller, BoxName.addHome, 'To Home');
}
},
onLongPress: () =>
_showChangeLocationDialog(controller, 'Home'),
),
],
), ),
), ],
AnimatedContainer( ),
);
}
}
/// A dedicated widget for the quick action buttons (Work/Home).
class _QuickActions extends StatelessWidget {
final MapPassengerController controller;
final String addWorkValue;
final String addHomeValue;
const _QuickActions({
required this.controller,
required this.addWorkValue,
required this.addHomeValue,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildQuickActionButton(
icon: Icons.work_outline,
text: addWorkValue == 'addWork' ? 'Add Work'.tr : 'To Work'.tr,
onTap: () {
if (addWorkValue == 'addWork') {
controller.workLocationFromMap = true;
controller.changeMainBottomMenuMap();
controller.changePickerShown();
} else {
_handleQuickAction(controller, BoxName.addWork, 'To Work');
}
},
onLongPress: () => _showChangeLocationDialog(controller, 'Work'),
),
_buildQuickActionButton(
icon: Icons.home_outlined,
text: addHomeValue == 'addHome' ? 'Add Home'.tr : 'To Home'.tr,
onTap: () {
if (addHomeValue == 'addHome') {
controller.homeLocationFromMap = true;
controller.changeMainBottomMenuMap();
controller.changePickerShown();
} else {
_handleQuickAction(controller, BoxName.addHome, 'To Home');
}
},
onLongPress: () => _showChangeLocationDialog(controller, 'Home'),
),
],
),
);
}
}
/// 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 {
@override
Widget build(BuildContext context) {
return GetBuilder<MapPassengerController>(
id: 'places_list', // Use a specific ID for targeted updates
builder: (controller) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
height: controller.placesDestination.isNotEmpty ? 300 : 0, height: controller.placesDestination.isNotEmpty ? 300 : 0,
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -137,20 +248,16 @@ GetBuilder<MapPassengerController> formSearchPlacesDestenation() {
margin: const EdgeInsets.symmetric(horizontal: 16.0), margin: const EdgeInsets.symmetric(horizontal: 16.0),
child: ListView.separated( child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const ClampingScrollPhysics(),
itemCount: controller.placesDestination.length, itemCount: controller.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) {
var res = controller.placesDestination[index]; final res = controller.placesDestination[index];
final title = res['name_ar'] ?? res['name'] ?? 'Unknown Place';
// استخراج البيانات حسب بنية السيرفر الجديد final address = res['address'] ?? 'Details not available';
var title = res['name'] ?? 'Unknown Place'; final latitude = res['latitude'];
var address = res['address'] ?? 'Unknown Address'; final longitude = res['longitude'];
var latitude = res['latitude'];
var longitude = res['longitude'];
var primaryCategory =
'Place'; // يمكن تطويره لاحقاً لو أضفت نوع للمكان
return ListTile( return ListTile(
leading: const Icon(Icons.place, size: 30, color: Colors.grey), leading: const Icon(Icons.place, size: 30, color: Colors.grey),
@@ -165,65 +272,105 @@ GetBuilder<MapPassengerController> formSearchPlacesDestenation() {
), ),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.favorite_border, color: Colors.grey), icon: const Icon(Icons.favorite_border, color: Colors.grey),
onPressed: () async { onPressed: () => _handleAddToFavorites(
if (latitude != null && longitude != null) { context, latitude, longitude, title),
await sql.insertMapLocation({
'latitude': latitude,
'longitude': longitude,
'name': title,
'rate': 'N/A',
}, TableName.placesFavorite);
Toast.show(
context,
'$title ${'Saved Successfully'.tr}',
AppColor.primaryColor,
);
} else {
Toast.show(
context,
'Invalid location data',
AppColor.redColor,
);
}
},
), ),
onTap: () async { onTap: () => _handlePlaceSelection(
if (latitude != null && longitude != null) { controller, latitude, longitude, title, index),
await sql.insertMapLocation({
'latitude': latitude,
'longitude': longitude,
'name': title,
'rate': 'N/A',
'createdAt': DateTime.now().toIso8601String(),
}, TableName.recentLocations);
controller.passengerLocation = controller.newMyLocation;
controller.myDestination = LatLng(latitude, longitude);
controller.convertHintTextDestinationNewPlaces(index);
controller.placesDestination = [];
controller.placeDestinationController.clear();
controller.changeMainBottomMenuMap();
controller.passengerStartLocationFromMap = true;
controller.isPickerShown = true;
} else {
Toast.show(
context,
'Invalid location data',
AppColor.redColor,
);
}
},
); );
}, },
), ),
), );
], },
), );
); }
// --- [تحسين] استخراج المنطق المعقد إلى دوال مساعدة ---
Future<void> _handleAddToFavorites(BuildContext context, dynamic latitude,
dynamic longitude, String title) async {
if (latitude != null && longitude != null) {
await sql.insertMapLocation({
'latitude': latitude,
'longitude': longitude,
'name': title,
'rate': 'N/A',
}, TableName.placesFavorite);
Toast.show(
context,
'$title ${'Saved Successfully'.tr}',
AppColor.primaryColor,
);
} else {
Toast.show(
context,
'Invalid location data',
AppColor.redColor,
);
}
}
Future<void> _handlePlaceSelection(MapPassengerController controller,
dynamic latitude, dynamic longitude, String title, int index) async {
if (latitude == null || longitude == null) {
Toast.show(Get.context!, 'Invalid location data', AppColor.redColor);
return;
}
// Save to recent locations
await sql.insertMapLocation({
'latitude': latitude,
'longitude': longitude,
'name': title,
'rate': 'N/A',
'createdAt': DateTime.now().toIso8601String(),
}, TableName.recentLocations);
final destLatLng = LatLng(
double.parse(latitude.toString()), double.parse(longitude.toString()));
if (controller.isAnotherOreder) {
// **Another Order Flow**
await _handleAnotherOrderSelection(controller, destLatLng);
} else {
// **Regular Order Flow**
_handleRegularOrderSelection(controller, destLatLng, index);
}
}
Future<void> _handleAnotherOrderSelection(
MapPassengerController controller, LatLng destination) async {
controller.myDestination = destination;
controller.clearPlacesDestination(); // Helper method in controller
await controller.getDirectionMap(
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
'${controller.myDestination.latitude},${controller.myDestination.longitude}');
controller.isPickerShown = false;
controller.passengerStartLocationFromMap = false;
controller.changeMainBottomMenuMap();
controller.showBottomSheet1();
}
void _handleRegularOrderSelection(
MapPassengerController controller, LatLng destination, int index) {
controller.passengerLocation = controller.newMyLocation;
controller.myDestination = destination;
controller.convertHintTextDestinationNewPlaces(index);
controller.clearPlacesDestination(); // Helper method in controller
controller.changeMainBottomMenuMap();
controller.passengerStartLocationFromMap = true;
controller.isPickerShown = true;
}
} }
// ---------------------------------------------------
// -- Helper Functions (kept from original code) --
// ---------------------------------------------------
Widget _buildQuickActionButton({ Widget _buildQuickActionButton({
required IconData icon, required IconData icon,
required String text, required String text,
@@ -280,22 +427,31 @@ void _showChangeLocationDialog(
void _handleQuickAction( void _handleQuickAction(
MapPassengerController controller, String boxName, String hintText) async { MapPassengerController controller, String boxName, String hintText) async {
final latLng = LatLng( // --- [تحسين] قراءة وتحويل الإحداثيات بأمان أكبر ---
double.parse(box.read(boxName).toString().split(',')[0]), try {
double.parse(box.read(boxName).toString().split(',')[1]), final locationString = box.read(boxName).toString();
); final parts = locationString.split(',');
controller.hintTextDestinationPoint = hintText; final latLng = LatLng(
controller.changeMainBottomMenuMap(); double.parse(parts[0]),
double.parse(parts[1]),
);
await controller.getDirectionMap( controller.hintTextDestinationPoint = hintText;
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}', controller.changeMainBottomMenuMap();
'${latLng.latitude},${latLng.longitude}',
); await controller.getDirectionMap(
controller.currentLocationToFormPlaces = false; '${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
controller.placesDestination = []; '${latLng.latitude},${latLng.longitude}',
controller.clearPlacesStart(); );
controller.clearPlacesDestination();
controller.passengerStartLocationFromMap = false; controller.currentLocationToFormPlaces = false;
controller.isPickerShown = false; controller.clearPlacesDestination(); // Helper method in controller
controller.showBottomSheet1(); controller.passengerStartLocationFromMap = false;
controller.isPickerShown = false;
controller.showBottomSheet1();
} catch (e) {
// Handle error if parsing fails
print("Error handling quick action: $e");
Toast.show(Get.context!, "Failed to get location".tr, AppColor.redColor);
}
} }

View File

@@ -1,12 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:Intaleq/constant/table_names.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart';
import '../../../constant/colors.dart'; import '../../../constant/colors.dart';
import '../../../constant/style.dart'; import '../../../constant/style.dart';
import '../../../controller/functions/toast.dart';
import '../../../controller/home/map_passenger_controller.dart'; import '../../../controller/home/map_passenger_controller.dart';
import '../../../main.dart';
// ---------------------------------------------------
// -- Widget for Start Point Search --
// ---------------------------------------------------
GetBuilder<MapPassengerController> formSearchPlacesStart() { GetBuilder<MapPassengerController> formSearchPlacesStart() {
return GetBuilder<MapPassengerController>( return GetBuilder<MapPassengerController>(
@@ -14,70 +16,47 @@ GetBuilder<MapPassengerController> formSearchPlacesStart() {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row( child: TextFormField(
children: [ controller: controller.placeStartController,
Expanded( onChanged: (value) {
child: TextFormField( if (controller.placeStartController.text.length > 2) {
controller: controller.placeStartController, controller.getPlacesStart();
onChanged: (value) { } else if (controller.placeStartController.text.isEmpty) {
if (controller.placeStartController.text.length > 5) { controller.clearPlacesStart();
// Reduced character limit }
controller.getPlacesStart(); },
controller.changeHeightStartPlaces(); decoration: InputDecoration(
} else if (controller.placeStartController.text.isEmpty) { hintText: 'Search for a starting point'.tr,
controller.clearPlacesStart(); hintStyle: AppStyle.subtitle.copyWith(color: Colors.grey[600]),
controller.changeHeightPlaces(); // Collapse if empty prefixIcon:
} const Icon(Icons.search, color: AppColor.primaryColor),
}, suffixIcon: controller.placeStartController.text.isNotEmpty
decoration: InputDecoration( ? IconButton(
hintText: controller.hintTextStartPoint, icon: Icon(Icons.clear, color: Colors.grey[400]),
hintStyle: onPressed: () {
AppStyle.subtitle.copyWith(color: Colors.grey[600]), controller.placeStartController.clear();
prefixIcon: controller.clearPlacesStart();
const Icon(Icons.search, color: AppColor.primaryColor), },
suffixIcon: controller.placeStartController.text.isNotEmpty )
? IconButton( : null,
icon: Icon(Icons.clear, color: Colors.grey[400]), contentPadding:
onPressed: () { const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
controller.placeStartController.clear(); border: OutlineInputBorder(
controller.clearPlacesStart(); borderRadius: BorderRadius.circular(8.0),
controller borderSide: BorderSide.none,
.changeHeightPlaces(); // Collapse on clear
},
)
: null,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 10.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(color: AppColor.primaryColor),
),
filled: true,
fillColor: Colors.grey[50],
),
),
), ),
const SizedBox(width: 8.0), focusedBorder: OutlineInputBorder(
IconButton( borderRadius: BorderRadius.circular(8.0),
onPressed: () { borderSide: BorderSide(color: AppColor.primaryColor),
controller.startLocationFromMap = true;
controller.changeMainBottomMenuMap();
controller.changePickerShown();
},
icon: Icon(Icons.location_on_outlined,
color: AppColor.accentColor, size: 30),
tooltip: 'Choose on Map',
), ),
], filled: true,
fillColor: Colors.grey[50],
),
), ),
), ),
AnimatedContainer( AnimatedContainer(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
height: controller.placesStart.isNotEmpty ? controller.height : 0, height: controller.placesStart.isNotEmpty ? 300 : 0,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
@@ -85,48 +64,33 @@ GetBuilder<MapPassengerController> formSearchPlacesStart() {
margin: const EdgeInsets.symmetric(horizontal: 16.0), margin: const EdgeInsets.symmetric(horizontal: 16.0),
child: ListView.separated( child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const ClampingScrollPhysics(),
itemCount: controller.placesStart.length, itemCount: controller.placesStart.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) {
var res = controller.placesStart[index]; var res = controller.placesStart[index];
var title = res['name_ar'] ?? res['name'] ?? 'Unknown Place';
var address = res['address'] ?? 'Details not available';
return ListTile( return ListTile(
leading: Image.network(res['icon'], width: 30, height: 30), leading: const Icon(Icons.place, size: 30, color: Colors.grey),
title: Text(res['name'].toString(), title: Text(title,
style: AppStyle.subtitle style: AppStyle.subtitle
.copyWith(fontWeight: FontWeight.w500)), .copyWith(fontWeight: FontWeight.w500)),
subtitle: Text(res['vicinity'].toString(), subtitle: Text(address,
style: TextStyle(color: Colors.grey[600], fontSize: 12)), style: TextStyle(color: Colors.grey[600], fontSize: 12)),
trailing: IconButton( onTap: () {
icon: const Icon(Icons.favorite_border, color: Colors.grey), var latitude = res['latitude'];
onPressed: () async { var longitude = res['longitude'];
await sql.insertMapLocation({ if (latitude != null && longitude != null) {
'latitude': res['geometry']['location']['lat'], controller.passengerLocation =
'longitude': res['geometry']['location']['lng'], LatLng(double.parse(latitude), double.parse(longitude));
'name': res['name'].toString(), controller.placeStartController.text = title;
'rate': res['rating'].toString(), controller.clearPlacesStart();
}, TableName.placesFavorite); // You might want to update the camera position on the map here
Toast.show( controller.update();
context, }
'${res['name']} ${'Saved Successfully'.tr}',
AppColor.primaryColor);
},
),
onTap: () async {
controller.changeHeightPlaces();
await sql.insertMapLocation({
'latitude': res['geometry']['location']['lat'],
'longitude': res['geometry']['location']['lng'],
'name': res['name'].toString(),
'rate': res['rating'].toString(),
'createdAt': DateTime.now().toIso8601String(),
}, TableName.recentLocations);
controller.convertHintTextStartNewPlaces(index);
controller.currentLocationString = res['name'];
controller.placesStart = [];
controller.placeStartController.clear();
}, },
); );
}, },

View File

@@ -1,4 +1,7 @@
import 'package:Intaleq/constant/box_name.dart';
import 'package:Intaleq/controller/firebase/firbase_messge.dart'; import 'package:Intaleq/controller/firebase/firbase_messge.dart';
import 'package:Intaleq/controller/firebase/notification_service.dart';
import 'package:Intaleq/main.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';
@@ -6,9 +9,12 @@ import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'dart:ui'; // مهم لإضافة تأثير الضبابية import 'dart:ui'; // مهم لإضافة تأثير الضبابية
import '../../../constant/colors.dart'; import '../../../constant/colors.dart';
import '../../../controller/functions/encrypt_decrypt.dart';
import '../../../controller/functions/tts.dart'; import '../../../controller/functions/tts.dart';
import '../../../controller/home/map_passenger_controller.dart'; import '../../../controller/home/map_passenger_controller.dart';
import '../../../controller/home/vip_waitting_page.dart'; import '../../../controller/home/vip_waitting_page.dart';
import '../../../env/env.dart';
import '../../../print.dart';
// --- الدالة الرئيسية بالتصميم الجديد --- // --- الدالة الرئيسية بالتصميم الجديد ---
GetBuilder<MapPassengerController> leftMainMenuIcons() { GetBuilder<MapPassengerController> leftMainMenuIcons() {
@@ -70,11 +76,11 @@ GetBuilder<MapPassengerController> leftMainMenuIcons() {
tooltip: 'VIP Waiting Page', tooltip: 'VIP Waiting Page',
onPressed: () => Get.to(() => VipWaittingPage()), onPressed: () => Get.to(() => VipWaittingPage()),
), ),
// _buildMapActionButton( _buildMapActionButton(
// icon: Octicons.ellipsis, icon: Octicons.ellipsis,
// tooltip: 'test', tooltip: 'test',
// onPressed: () => Get.to(() => TestPage()), onPressed: () => Get.to(() => TestPage()),
// ), ),
], ],
), ),
), ),
@@ -118,15 +124,20 @@ class TestPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final firebaseMessagesController =
Get.isRegistered<FirebaseMessagesController>()
? Get.find<FirebaseMessagesController>()
: Get.put(FirebaseMessagesController());
return Scaffold( return Scaffold(
appBar: AppBar(), appBar: AppBar(),
body: Center( body: Center(
child: TextButton( child: TextButton(
onPressed: () async {}, onPressed: () async {
var token =
'e4t5mB-WTsyhi2M0v5AOAy:APA91bGmQG8gcitcJB7x69oHCweCn44NdljP5ZVlO1IK62w62Gac4dCIjE3SMFPV6YcFdTMQrRHE1BXnbktEM19JE4xjcEyLz-GwC1HrCbDl2X24d4PfrPQ';
NotificationService.sendNotification(
target: token,
title: 'Hi ,I will go now',
body: 'A passenger is waiting for you.',
isTopic: false, // Important: this is a token
);
},
child: Text( child: Text(
"Text Button", "Text Button",
), ),

View File

@@ -968,14 +968,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.14+2" version: "0.5.14+2"
google_polyline_algorithm:
dependency: "direct main"
description:
name: google_polyline_algorithm
sha256: "357874f00d3f93c3ba1bf4b4d9a154aa9ee87147c068238c1e8392012b686a03"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
google_sign_in: google_sign_in:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -25,7 +25,7 @@ dependencies:
get_storage: ^2.1.1 get_storage: ^2.1.1
url_launcher: ^6.1.20 url_launcher: ^6.1.20
location: ^8.0.1 location: ^8.0.1
google_polyline_algorithm: ^3.1.0 # google_polyline_algorithm: ^3.1.0
animated_text_kit: ^4.2.2 animated_text_kit: ^4.2.2
flutter_secure_storage: ^10.0.0-beta.4 flutter_secure_storage: ^10.0.0-beta.4
geolocator: ^14.0.2 geolocator: ^14.0.2