25-10-5/1
This commit is contained in:
39
lib/app_bindings.dart
Normal file
39
lib/app_bindings.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -160,9 +160,9 @@ class LoginController extends GetxController {
|
||||
Uri.parse(AppLink.loginJwtRider),
|
||||
body: payload,
|
||||
);
|
||||
// Log.print('req: ${response1.request}');
|
||||
// Log.print('response: ${response1.body}');
|
||||
// Log.print('payload: ${payload}');
|
||||
Log.print('req: ${response1.request}');
|
||||
Log.print('response: ${response1.body}');
|
||||
Log.print('payload: ${payload}');
|
||||
// Log.print('decodedResponse1: ${jsonDecode(response1.body)}');
|
||||
|
||||
if (response1.statusCode == 200) {
|
||||
|
||||
@@ -69,7 +69,9 @@ class FirebaseMessagesController extends GetxController {
|
||||
}
|
||||
|
||||
NotificationController notificationController =
|
||||
Get.put(NotificationController());
|
||||
Get.isRegistered<NotificationController>()
|
||||
? Get.find<NotificationController>()
|
||||
: Get.put(NotificationController());
|
||||
|
||||
Future getTokens() async {
|
||||
String? basicAuthCredentials =
|
||||
@@ -96,7 +98,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
|
||||
Future getToken() async {
|
||||
fcmToken.getToken().then((token) {
|
||||
Log.print('fcmToken: ${token}');
|
||||
// Log.print('fcmToken: ${token}');
|
||||
box.write(BoxName.tokenFCM, (token.toString()));
|
||||
});
|
||||
|
||||
@@ -125,16 +127,17 @@ class FirebaseMessagesController extends GetxController {
|
||||
|
||||
Future<void> fireBaseTitles(RemoteMessage message) async {
|
||||
if (message.notification!.title! == 'Order'.tr) {
|
||||
Log.print('message: ${message}');
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(
|
||||
'Order', message.notification!.body!, 'Order');
|
||||
'Order'.tr, message.notification!.body!, 'Order');
|
||||
}
|
||||
} else if (message.notification!.title! == 'Accepted Ride') {
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(
|
||||
'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>;
|
||||
Log.print('myList: ${myList}');
|
||||
@@ -513,7 +516,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
// 'DriverList': jsonEncode(data),
|
||||
// },
|
||||
'android': {
|
||||
'priority': 'high', // Set priority to high
|
||||
'priority': 'HIGH ', // Set priority to high
|
||||
'notification': {
|
||||
'sound': tone,
|
||||
},
|
||||
@@ -549,7 +552,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
// 'body': body,
|
||||
// 'sound': 'true'
|
||||
// },
|
||||
// 'priority': 'high',
|
||||
// 'priority': 'HIGH ',
|
||||
// 'data': <String, dynamic>{
|
||||
// 'click_action': 'FLUTTER_NOTIFICATION_CLICK',
|
||||
// 'id': '1',
|
||||
@@ -581,7 +584,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
|
||||
Future<void> sendNotificationToDriverMAP(
|
||||
String title, String body, String token, List<String> data, String tone,
|
||||
{int retryCount = 2}) async {
|
||||
{int retryCount = 1}) async {
|
||||
try {
|
||||
if (serviceAccountKeyJson.isEmpty) {
|
||||
print("🔴 Error: Service Account Key is empty");
|
||||
@@ -590,8 +593,8 @@ class FirebaseMessagesController extends GetxController {
|
||||
|
||||
// Initialize AccessTokenManager
|
||||
final accessTokenManager = AccessTokenManager(serviceAccountKeyJson);
|
||||
Log.print(
|
||||
'accessTokenManager: ${accessTokenManager.serviceAccountJsonKey}');
|
||||
// Log.print(
|
||||
// 'accessTokenManager: ${accessTokenManager.serviceAccountJsonKey}');
|
||||
|
||||
// Obtain an OAuth 2.0 access token
|
||||
final accessToken = await accessTokenManager.getAccessToken();
|
||||
@@ -616,7 +619,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
'DriverList': jsonEncode(data),
|
||||
},
|
||||
'android': {
|
||||
'priority': 'high', // Set priority to high
|
||||
'priority': 'HIGH ', // Set priority to high
|
||||
'notification': {
|
||||
'sound': tone,
|
||||
},
|
||||
@@ -693,7 +696,7 @@ class FirebaseMessagesController extends GetxController {
|
||||
'body': body,
|
||||
},
|
||||
'android': {
|
||||
'priority': 'high', // Set priority to high
|
||||
'priority': 'HIGH ', // Set priority to high
|
||||
'notification': {
|
||||
'sound': tone,
|
||||
},
|
||||
|
||||
71
lib/controller/firebase/notification_service.dart
Normal file
71
lib/controller/firebase/notification_service.dart
Normal 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
|
||||
);
|
||||
}
|
||||
@@ -123,6 +123,7 @@ class CRUD {
|
||||
|
||||
// 401 → دع الطبقة العليا تتعامل مع التجديد
|
||||
if (sc == 401) {
|
||||
await Get.put(LoginController()).getJWT();
|
||||
// لا تستدع getJWT هنا كي لا نضاعف الرحلات
|
||||
return 'token_expired';
|
||||
}
|
||||
@@ -190,6 +191,10 @@ class CRUD {
|
||||
'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) {
|
||||
var jsonData = jsonDecode(response.body);
|
||||
if (jsonData['status'] == 'success') {
|
||||
|
||||
62
lib/controller/home/decode_polyline_isolate.dart
Normal file
62
lib/controller/home/decode_polyline_isolate.dart
Normal 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;
|
||||
// }
|
||||
@@ -6,6 +6,7 @@ import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
import 'dart:convert';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:Intaleq/constant/univeries_polygon.dart';
|
||||
@@ -22,7 +23,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:get/get.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:location/location.dart';
|
||||
import 'package:Intaleq/constant/colors.dart';
|
||||
@@ -52,6 +53,7 @@ import '../functions/launch.dart';
|
||||
import '../functions/package_info.dart';
|
||||
import '../functions/secure_storage.dart';
|
||||
import '../payment/payment_controller.dart';
|
||||
import 'decode_polyline_isolate.dart';
|
||||
import 'deep_link_controller.dart';
|
||||
import 'device_tier.dart';
|
||||
import 'vip_waitting_page.dart';
|
||||
@@ -1035,7 +1037,7 @@ class MapPassengerController extends GetxController {
|
||||
if (isBeginRideFromDriverRunning) return; // Prevent duplicate streams
|
||||
isBeginRideFromDriverRunning = true;
|
||||
|
||||
Timer.periodic(const Duration(seconds: 1), (timer) async {
|
||||
Timer.periodic(const Duration(seconds: 2), (timer) async {
|
||||
try {
|
||||
var res = await CRUD().get(
|
||||
link: AppLink.getRideStatusBegin, payload: {'ride_id': rideId});
|
||||
@@ -1138,13 +1140,14 @@ class MapPassengerController extends GetxController {
|
||||
|
||||
Map<String, dynamic> tripData =
|
||||
box.read(BoxName.tripData) as Map<String, dynamic>;
|
||||
final points = decodePolyline(
|
||||
tripData["routes"][0]["overview_polyline"]["points"]);
|
||||
String pointsString =
|
||||
(tripData["routes"][0]["overview_polyline"]["points"]);
|
||||
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
double lat = points[i][0].toDouble();
|
||||
double lng = points[i][1].toDouble();
|
||||
polylineCoordinates.add(LatLng(lat, lng));
|
||||
List<LatLng> decodedPoints =
|
||||
await compute(decodePolylineIsolate, pointsString);
|
||||
// decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
|
||||
for (int i = 0; i < decodedPoints.length; i++) {
|
||||
polylineCoordinates.add(decodedPoints[i]);
|
||||
}
|
||||
var polyline = Polyline(
|
||||
polylineId: const PolylineId('begin trip'),
|
||||
@@ -1819,6 +1822,7 @@ class MapPassengerController extends GetxController {
|
||||
await getCarsLocationByPassengerAndReloadMarker(
|
||||
box.read(BoxName.carType), reloadDuration);
|
||||
// await getNearestDriverByPassengerLocation();
|
||||
currentTimeSearchingCaptainWindow = 0;
|
||||
driversStatusForSearchWindow = 'We are search for nearst driver'.tr;
|
||||
if (isDriversDataValid()) {
|
||||
driversFound = true;
|
||||
@@ -2142,8 +2146,9 @@ class MapPassengerController extends GetxController {
|
||||
} else if (rideStatusDelayed == 'Apply' ||
|
||||
rideStatusDelayed == 'Applied') {
|
||||
isApplied = true;
|
||||
rideAppliedFromDriver(isApplied);
|
||||
timer.cancel();
|
||||
rideAppliedFromDriver(isApplied);
|
||||
// timer.cancel();
|
||||
// Close stream after applying
|
||||
} else if (attemptCounter >= maxAttempts ||
|
||||
rideStatusDelayed == 'waiting') {
|
||||
@@ -2186,7 +2191,7 @@ class MapPassengerController extends GetxController {
|
||||
rideAppliedFromDriver(bool isApplied) async {
|
||||
await getUpdatedRideForDriverApply(rideId);
|
||||
NotificationController().showNotification(
|
||||
'Accepted Ride'.tr,
|
||||
'Accepted Ride',
|
||||
'$driverName ${'accepted your order at price'.tr} ${totalPassenger.toStringAsFixed(1)} ${'with type'.tr} ${box.read(BoxName.carType)}',
|
||||
'ding');
|
||||
if (box.read(BoxName.carType) == 'Speed' ||
|
||||
@@ -2306,7 +2311,7 @@ class MapPassengerController extends GetxController {
|
||||
// driversToken.remove(driverToken);
|
||||
// for (var i = 1; i < driversToken.length; i++) {
|
||||
firebaseMessagesController.sendNotificationToDriverMAP(
|
||||
'Order Accepted'.tr,
|
||||
'Order Accepted',
|
||||
'$driverName${'Accepted your order'.tr}',
|
||||
driverToken.toString(),
|
||||
[],
|
||||
@@ -3514,57 +3519,71 @@ class MapPassengerController extends GetxController {
|
||||
|
||||
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض
|
||||
|
||||
double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
|
||||
const R = 6371.0; // km
|
||||
final dLat = (lat2 - lat1) * math.pi / 180.0;
|
||||
final dLon = (lon2 - lon1) * math.pi / 180.0;
|
||||
final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||||
math.cos(lat1 * math.pi / 180.0) *
|
||||
math.cos(lat2 * math.pi / 180.0) *
|
||||
math.sin(dLon / 2) *
|
||||
math.sin(dLon / 2);
|
||||
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
||||
return R * c;
|
||||
}
|
||||
// double _haversineKm(double lat1, double lon1, double lat2, double lon2) {
|
||||
// const R = 6371.0; // km
|
||||
// final dLat = (lat2 - lat1) * math.pi / 180.0;
|
||||
// final dLon = (lon2 - lon1) * math.pi / 180.0;
|
||||
// final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||||
// math.cos(lat1 * math.pi / 180.0) *
|
||||
// math.cos(lat2 * math.pi / 180.0) *
|
||||
// math.sin(dLon / 2) *
|
||||
// math.sin(dLon / 2);
|
||||
// final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
||||
// return R * c;
|
||||
// }
|
||||
|
||||
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات عرض
|
||||
double _kmToLatDelta(double km) => km / 111.0;
|
||||
// double _kmToLatDelta(double km) => km / 111.0;
|
||||
|
||||
/// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض)
|
||||
double _kmToLngDelta(double km, double atLat) =>
|
||||
km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9);
|
||||
// /// تحويل نصف قطر بالكيلومتر إلى دلتا درجات طول (تعتمد على خط العرض)
|
||||
// double _kmToLngDelta(double km, double atLat) =>
|
||||
// km / (111.320 * math.cos(atLat * math.pi / 180.0)).abs().clamp(1e-6, 1e9);
|
||||
|
||||
/// حساب درجة التطابق النصي (كل كلمة تبدأ بها الاسم = 2 نقاط، يحتويها = 1 نقطة)
|
||||
double _relevanceScore(String name, String query) {
|
||||
final n = name.toLowerCase();
|
||||
final parts =
|
||||
query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2);
|
||||
double s = 0.0;
|
||||
for (final p in parts) {
|
||||
if (n.startsWith(p)) {
|
||||
s += 2.0;
|
||||
} else if (n.contains(p)) {
|
||||
s += 1.0;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
// double _relevanceScore(String name, String query) {
|
||||
// final n = name.toLowerCase();
|
||||
// final parts =
|
||||
// query.toLowerCase().split(RegExp(r'\s+')).where((p) => p.length >= 2);
|
||||
// double s = 0.0;
|
||||
// for (final p in parts) {
|
||||
// if (n.startsWith(p)) {
|
||||
// s += 2.0;
|
||||
// } else if (n.contains(p)) {
|
||||
// s += 1.0;
|
||||
// }
|
||||
// }
|
||||
// return s;
|
||||
// }
|
||||
// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها
|
||||
// انسخ هذه الدوال والصقها داخل كلاس الكنترولر الخاص بك
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// --== الدالة الرئيسية للبحث ==--
|
||||
// -----------------------------------------------------------------
|
||||
/// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها
|
||||
// انسخ هذه الدوال والصقها داخل كلاس الكنترولر الخاص بك
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// --== الدالة الرئيسية للبحث ==--
|
||||
// -----------------------------------------------------------------
|
||||
/// الدالة الرئيسية لجلب الأماكن من السيرفر وترتيبها
|
||||
Future<void> getPlaces() async {
|
||||
// افترض وجود `placeDestinationController` و `passengerLocation` و `CRUD()` معرفة في الكنترولر
|
||||
final q = placeDestinationController.text.trim();
|
||||
if (q.isEmpty) {
|
||||
if (q.isEmpty || q.length < 3) {
|
||||
// يفضل عدم البحث قبل 3 أحرف
|
||||
placesDestination = [];
|
||||
update();
|
||||
update(); // افترض أنك تستخدم GetX أو أي State Management آخر
|
||||
return;
|
||||
}
|
||||
|
||||
final lat = passengerLocation.latitude;
|
||||
final lng = passengerLocation.longitude;
|
||||
|
||||
// نصف قطر البحث بالكيلومتر (عدّل حسب رغبتك)
|
||||
// نصف قطر البحث بالكيلومتر
|
||||
const radiusKm = 200.0;
|
||||
|
||||
// حساب الباوند الصحيح (درجات، وليس 2.2 درجة ثابتة)
|
||||
// حساب النطاق الجغرافي (Bounding Box) لإرساله للسيرفر
|
||||
final latDelta = _kmToLatDelta(radiusKm);
|
||||
final lngDelta = _kmToLngDelta(radiusKm, lat);
|
||||
|
||||
@@ -3574,6 +3593,7 @@ class MapPassengerController extends GetxController {
|
||||
final lngMax = lng + lngDelta;
|
||||
|
||||
try {
|
||||
// استدعاء الـ API (تأكد من أن AppLink.getPlacesSyria يشير للسكريبت الجديد)
|
||||
final response = await CRUD().post(
|
||||
link: AppLink.getPlacesSyria,
|
||||
payload: {
|
||||
@@ -3585,59 +3605,106 @@ class MapPassengerController extends GetxController {
|
||||
},
|
||||
);
|
||||
|
||||
// يدعم شكلي استجابة: إما {"...","message":[...]} أو قائمة مباشرة [...]
|
||||
// --- [تم الإصلاح هنا] ---
|
||||
// معالجة الاستجابة من السيرفر بشكل يوافق {"status":"success", "message":[...]}
|
||||
List list;
|
||||
if (response is Map && response['message'] is List) {
|
||||
list = List.from(response['message'] as 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');
|
||||
print('Unexpected response shape from server');
|
||||
return;
|
||||
}
|
||||
|
||||
// جهّز الحقول المحتملة للأسماء
|
||||
// --- هنا يبدأ عمل فلاتر: الترتيب النهائي الدقيق ---
|
||||
|
||||
// دالة مساعدة لاختيار أفضل اسم متاح
|
||||
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) {
|
||||
final plat = double.tryParse(p['latitude']?.toString() ?? '') ?? 0.0;
|
||||
final plng = double.tryParse(p['longitude']?.toString() ?? '') ?? 0.0;
|
||||
final plat = double.tryParse(p['latitude']?.toString() ?? '0.0') ?? 0.0;
|
||||
final plng =
|
||||
double.tryParse(p['longitude']?.toString() ?? '0.0') ?? 0.0;
|
||||
|
||||
final d = _haversineKm(lat, lng, plat, plng);
|
||||
final rel = _relevanceScore(_bestName(p), q);
|
||||
final distance = _haversineKm(lat, lng, plat, plng);
|
||||
final relevance = _relevanceScore(_bestName(p), q);
|
||||
|
||||
// معادلة ترتيب ذكية: مسافة أقل + تطابق أعلى = نقاط أعلى
|
||||
// تضيف +1 لضمان عدم وصول الوزن للصفر عند عدم وجود تطابق
|
||||
final score = (1.0 / (1.0 + d)) * (1.0 + rel);
|
||||
// معادلة الترتيب: (الأولوية للمسافة الأقرب) * (ثم الصلة الأعلى)
|
||||
final score = (1.0 / (1.0 + distance)) * (1.0 + relevance);
|
||||
|
||||
p['distanceKm'] = d;
|
||||
p['relevance'] = rel;
|
||||
p['distanceKm'] = distance;
|
||||
p['relevance'] = relevance;
|
||||
p['score'] = score;
|
||||
}
|
||||
|
||||
// رتّب حسب score تنازليًا، ثم المسافة تصاعديًا كحسم
|
||||
// ترتيب القائمة النهائية حسب النقاط (الأعلى أولاً)
|
||||
list.sort((a, b) {
|
||||
final sa = (a['score'] ?? 0.0) as double;
|
||||
final sb = (b['score'] ?? 0.0) as double;
|
||||
final cmp = 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);
|
||||
return sb.compareTo(sa);
|
||||
});
|
||||
|
||||
// خذ أول 10–15 للعرض (اختياري)، أو اعرض الكل
|
||||
placesDestination = list.take(15).toList();
|
||||
Log.print('placesDestination: $placesDestination');
|
||||
placesDestination = list;
|
||||
print('Updated places: $placesDestination');
|
||||
update();
|
||||
} catch (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;
|
||||
|
||||
// // تحديد اللغة حسب الإدخال
|
||||
@@ -3786,27 +3853,102 @@ class MapPassengerController extends GetxController {
|
||||
// update();
|
||||
// }
|
||||
|
||||
Future getPlacesStart() async {
|
||||
var languageCode = wayPoint0Controller.text;
|
||||
|
||||
// Regular expression to check for English alphabet characters
|
||||
final englishRegex = RegExp(r'[a-zA-Z]');
|
||||
|
||||
// Check if text contains English characters
|
||||
if (englishRegex.hasMatch(languageCode)) {
|
||||
languageCode = 'en';
|
||||
} else {
|
||||
languageCode = 'ar';
|
||||
Future<void> getPlacesStart() async {
|
||||
// افترض وجود `placeDestinationController` و `passengerLocation` و `CRUD()` معرفة في الكنترولر
|
||||
final q = placeStartController.text.trim();
|
||||
if (q.isEmpty || q.length < 3) {
|
||||
// يفضل عدم البحث قبل 3 أحرف
|
||||
placesStart = [];
|
||||
update(); // افترض أنك تستخدم GetX أو أي State Management آخر
|
||||
return;
|
||||
}
|
||||
|
||||
var url =
|
||||
// '${AppLink.googleMapsLink}place/nearbysearch/json?location=${mylocation.longitude}&radius=25000&language=ar&keyword=&key=${placeController.text}${AK.mapAPIKEY}';
|
||||
'${AppLink.googleMapsLink}place/nearbysearch/json?keyword=${placeStartController.text}&location=${passengerLocation.latitude},${passengerLocation.longitude}&radius=250000&language=$languageCode&key=${AK.mapAPIKEY.toString()}';
|
||||
final lat = passengerLocation.latitude;
|
||||
final lng = passengerLocation.longitude;
|
||||
|
||||
var response = await CRUD().getGoogleApi(link: url, payload: {});
|
||||
// نصف قطر البحث بالكيلومتر
|
||||
const radiusKm = 200.0;
|
||||
|
||||
placesStart = response['results'];
|
||||
update();
|
||||
// حساب النطاق الجغرافي (Bounding Box) لإرساله للسيرفر
|
||||
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 {
|
||||
@@ -4557,12 +4699,15 @@ class MapPassengerController extends GetxController {
|
||||
data[0]["start_location"]['lat'], data[0]["start_location"]['lng']);
|
||||
|
||||
durationToRide = data[0]['duration']['value'];
|
||||
final points =
|
||||
decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
double lat = points[i][0].toDouble();
|
||||
double lng = points[i][1].toDouble();
|
||||
polylineCoordinates.add(LatLng(lat, lng));
|
||||
final String pointsString =
|
||||
response['routes'][0]["overview_polyline"]["points"];
|
||||
List<LatLng> decodedPoints =
|
||||
await compute(decodePolylineIsolate, pointsString);
|
||||
// decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
|
||||
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
|
||||
|
||||
@@ -4609,18 +4754,18 @@ class MapPassengerController extends GetxController {
|
||||
position: LatLng(
|
||||
data[0]["end_location"]['lat'], data[0]["end_location"]['lng']),
|
||||
icon: endIcon,
|
||||
// infoWindow: InfoWindow(
|
||||
// title: endNameAddress,
|
||||
// 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}'}'),
|
||||
infoWindow: InfoWindow(
|
||||
title: endNameAddress,
|
||||
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}'}'),
|
||||
),
|
||||
);
|
||||
// // Show info windows automatically
|
||||
// Future.delayed(const Duration(milliseconds: 500), () {
|
||||
// mapController?.showMarkerInfoWindow(const MarkerId('start'));
|
||||
// });
|
||||
// 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();
|
||||
|
||||
@@ -4632,11 +4777,23 @@ class MapPassengerController extends GetxController {
|
||||
false; // الأفضل أن يكون الافتراضي هو الجودة العالية
|
||||
|
||||
// نمرر عدد النقاط المناسب هنا
|
||||
animatePolylineLayered(
|
||||
polylineCoordinates,
|
||||
maxPoints:
|
||||
lowEndMode ? 30 : 150, // 30 نقطة لوضع الأداء، 150 للوضع العادي
|
||||
);
|
||||
if (Platform.isIOS) {
|
||||
animatePolylineLayered(
|
||||
polylineCoordinates,
|
||||
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;
|
||||
isMarkersShown = true;
|
||||
@@ -4672,7 +4829,7 @@ class MapPassengerController extends GetxController {
|
||||
|
||||
// 2) رسم متدرّج بطبقات متراكبة (بدون حذف)، برونزي ↔ أخضر، مع zIndex وعرض مختلف
|
||||
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_'));
|
||||
update();
|
||||
@@ -4682,8 +4839,8 @@ class MapPassengerController extends GetxController {
|
||||
if (coords.length < 2) return;
|
||||
|
||||
// ألوان مع شفافية خفيفة للتمييز
|
||||
Color bronze([int alpha = 220]) => const Color(0xFFCD7F32).withAlpha(alpha);
|
||||
Color green([int alpha = 220]) => Colors.green.withAlpha(alpha);
|
||||
Color bronze([int alpha = 220]) => AppColor.gold;
|
||||
Color green([int alpha = 220]) => AppColor.primaryColor;
|
||||
|
||||
Color _layerColor(int layer) => (layer % 2 == 0) ? bronze() : green();
|
||||
|
||||
@@ -4818,14 +4975,16 @@ class MapPassengerController extends GetxController {
|
||||
distance = distanceOfDestination + (data[0]['distance']['value']) / 1000;
|
||||
|
||||
update();
|
||||
final points =
|
||||
decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
|
||||
for (int i = 0; i < points.length; i++) {
|
||||
if (points[i][0].toString() != '') {
|
||||
double lat = points[i][0].toDouble();
|
||||
double lng = points[i][1].toDouble();
|
||||
polylineCoordinatesPointsAll[index].add(LatLng(lat, lng));
|
||||
}
|
||||
// final points =
|
||||
// decodePolyline(response["routes"][0]["overview_polyline"]["points"]);
|
||||
final String pointsString =
|
||||
response['routes'][0]["overview_polyline"]["points"];
|
||||
|
||||
List<LatLng> decodedPoints =
|
||||
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
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:Intaleq/controller/functions/crud.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.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/controller/auth/login_controller.dart';
|
||||
import 'package:Intaleq/controller/functions/secure_storage.dart';
|
||||
import 'package:Intaleq/views/widgets/my_scafold.dart';
|
||||
import 'package:Intaleq/constant/style.dart';
|
||||
import 'package:quick_actions/quick_actions.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
|
||||
with GetTickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
|
||||
// الحركات الخاصة بكل عنصر من عناصر الواجهة
|
||||
late Animation<double> titleFadeAnimation;
|
||||
late Animation<double> titleScaleAnimation;
|
||||
late Animation<double> taglineFadeAnimation;
|
||||
late Animation<double> titleFadeAnimation,
|
||||
titleScaleAnimation,
|
||||
taglineFadeAnimation,
|
||||
footerFadeAnimation;
|
||||
late Animation<Offset> taglineSlideAnimation;
|
||||
late Animation<double> footerFadeAnimation;
|
||||
|
||||
final progress = 0.0.obs;
|
||||
Timer? _progressTimer;
|
||||
@@ -30,111 +31,160 @@ class SplashScreenController extends GetxController
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
);
|
||||
vsync: this, duration: const Duration(milliseconds: 2000));
|
||||
|
||||
// --- تعريف الحركات المتتالية ---
|
||||
// Animation definitions
|
||||
titleFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut)));
|
||||
titleScaleAnimation = Tween<double>(begin: 0.8, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.0, 0.5, curve: Curves.easeOut)));
|
||||
taglineFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.3, 0.8, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.3, 0.8, curve: Curves.easeOut)));
|
||||
taglineSlideAnimation =
|
||||
Tween<Offset>(begin: const Offset(0, 0.5), end: Offset.zero).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.3, 0.8, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.3, 0.8, curve: Curves.easeOut)));
|
||||
footerFadeAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.5, 1.0, curve: Curves.easeOut),
|
||||
),
|
||||
);
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: const Interval(0.5, 1.0, curve: Curves.easeOut)));
|
||||
|
||||
_animationController.forward();
|
||||
|
||||
// بدء عملية التهيئة والتحميل
|
||||
_initializeApp();
|
||||
}
|
||||
|
||||
/// تهيئة التطبيق وانتظار انتهاء التحميل قبل الانتقال
|
||||
Future<void> _initializeApp() async {
|
||||
// تشغيل مؤقت شريط التقدم ليعطي انطباعاً مرئياً فقط
|
||||
_startProgressAnimation();
|
||||
|
||||
// تعريف مهمتين: منطق التحميل، والحد الأدنى لوقت عرض الشاشة
|
||||
// Run navigation logic and background services in parallel.
|
||||
final logicFuture = _performNavigationLogic();
|
||||
final backgroundServicesFuture = _initializeBackgroundServices();
|
||||
|
||||
// Ensure the splash screen is visible for a minimum duration.
|
||||
final minTimeFuture = Future.delayed(const Duration(seconds: 3));
|
||||
|
||||
// الانتظار حتى انتهاء المهمتين معاً
|
||||
await Future.wait([logicFuture, minTimeFuture]);
|
||||
|
||||
// بعد انتهاء الانتظار، سيتم الانتقال تلقائياً من داخل a_performNavigationLogic
|
||||
// Wait for all tasks to complete.
|
||||
await Future.wait([logicFuture, backgroundServicesFuture, minTimeFuture]);
|
||||
}
|
||||
|
||||
/// تشغيل حركة شريط التقدم بشكل مرئي ومنفصل عن منطق التحميل
|
||||
void _startProgressAnimation() {
|
||||
const totalTime = 2800; // مدة ملء الشريط
|
||||
const interval = 50;
|
||||
// Visual timer for the progress bar.
|
||||
const totalTime = 2800; // ms
|
||||
const interval = 50; // ms
|
||||
int elapsed = 0;
|
||||
|
||||
_progressTimer =
|
||||
Timer.periodic(const Duration(milliseconds: interval), (timer) {
|
||||
elapsed += interval;
|
||||
progress.value = (elapsed / totalTime).clamp(0.0, 1.0);
|
||||
if (elapsed >= totalTime) {
|
||||
timer.cancel();
|
||||
}
|
||||
if (elapsed >= totalTime) 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 {
|
||||
// تنفيذ المهام الأولية
|
||||
await _getPackageInfo();
|
||||
SecureStorage().saveData('iss', 'mobile-app:');
|
||||
|
||||
// تحديد الشاشة التالية
|
||||
if (box.read(BoxName.onBoarding) == null) {
|
||||
Get.off(() => OnBoardingPage());
|
||||
} else if (box.read(BoxName.email) != null &&
|
||||
box.read(BoxName.phone) != null &&
|
||||
box.read(BoxName.isVerified) == '1') {
|
||||
// -- النقطة الأهم --
|
||||
// هنا ننتظر انتهاء عملية تسجيل الدخول قبل الانتقال
|
||||
await Get.put(LoginController()).loginUsingCredentials(
|
||||
// `Get.find()` creates the LoginController instance here.
|
||||
final loginController = Get.find<LoginController>();
|
||||
// The loginController itself will handle navigation via Get.offAll() upon success.
|
||||
await loginController.loginUsingCredentials(
|
||||
box.read(BoxName.passengerID).toString(),
|
||||
box.read(BoxName.email).toString(),
|
||||
);
|
||||
// بعد هذه العملية، سيتولى LoginController بنفسه الانتقال للصفحة الرئيسية أو صفحة الدخول
|
||||
} else {
|
||||
Get.off(() => LoginPage());
|
||||
}
|
||||
}
|
||||
|
||||
/// جلب معلومات الحزمة لعرض إصدار التطبيق
|
||||
Future<void> _getPackageInfo() async {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
box.write(BoxName.packagInfo, info.version);
|
||||
update();
|
||||
try {
|
||||
final info = await PackageInfo.fromPlatform();
|
||||
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
|
||||
@@ -144,40 +194,3 @@ class SplashScreenController extends GetxController
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,47 +8,65 @@ class MyTranslation extends Translations {
|
||||
"SYP": "ل.س",
|
||||
"Order": "طلب",
|
||||
"OrderVIP": "طلب VIP",
|
||||
"Hi ,I Arrive your site": "أنا وصلت لموقعك",
|
||||
"Cancel Trip": "إلغاء الرحلة",
|
||||
"Passenger Cancel Trip": "الراكب لغى الرحلة",
|
||||
"Passenger Cancel Trip": "الراكب ألغى الرحلة",
|
||||
"VIP Order": "طلب VIP",
|
||||
"The driver accepted your trip": "الكابتن وافق على رحلتك",
|
||||
'Hi ,I Arrive your site': "مرحبًا، لقد وصلت إلى موقعك",
|
||||
"The driver accepted your trip": "السائق قبل رحلتك",
|
||||
"message From passenger": "رسالة من الراكب",
|
||||
"Cancel": "إلغاء",
|
||||
"Trip Cancelled. The cost of the trip will be added to your wallet.":
|
||||
"تم إلغاء الرحلة. ستتم إضافة تكلفة الرحلة إلى محفظتك.",
|
||||
"تم إلغاء الرحلة. سيتم إضافة تكلفة الرحلة إلى محفظتك.",
|
||||
"token change": "تغيير الرمز",
|
||||
"face detect": "التحقق من الوجه",
|
||||
"Face Detection Result": "نتيجة التحقق من الوجه",
|
||||
"similar": "مطابق",
|
||||
"not similar": "غير مطابق",
|
||||
"Hi ,I will go now": "مرحباً، أنا ذاهب الآن",
|
||||
"face detect": "كشف الوجه",
|
||||
"Face Detection Result": "نتيجة كشف الوجه",
|
||||
"similar": "مشابه",
|
||||
"not similar": "غير مشابه",
|
||||
"Hi ,I will go now": "مرحبًا، سأذهب الآن",
|
||||
"Passenger come to you": "الراكب قادم إليك",
|
||||
"Call Income": "مكالمة واردة",
|
||||
"Call Income from Passenger": "مكالمة واردة من الراكب",
|
||||
"Criminal Document Required": "مطلوب وثيقة لا حكم عليه",
|
||||
"You should have upload it .": "يجب عليك رفعها.",
|
||||
"Criminal Document Required": "مطلوب وثيقة جنائية",
|
||||
"You should have upload it .": "يجب عليك تحميلها.",
|
||||
"Call End": "انتهاء المكالمة",
|
||||
"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.":
|
||||
"نأسف لإبلاغك بأن كابتن آخر قد قبل هذا الطلب.",
|
||||
"Driver Applied the Ride for You": "الكابتن قام بطلب الرحلة لك",
|
||||
"نأسف لإعلامك بأن سائقًا آخر قد قبل هذا الطلب.",
|
||||
"Driver Applied the Ride for You": "السائق قدم الطلب لك",
|
||||
"Applied": "تم التقديم",
|
||||
"My Wallet": "محفظتي",
|
||||
"Top up Balance": "تعبئة الرصيد",
|
||||
"Please go to Car Driver": "الرجاء التوجه إلى سيارة الكابتن",
|
||||
"Ok I will go now.": "حسناً، أنا ذاهب الآن.",
|
||||
'Pay by Sham Cash': 'الدفع عبر شام كاش',
|
||||
'Pay with Debit Card': 'الدفع ببطاقة الخصم',
|
||||
"Please go to Car Driver": "يرجى الذهاب إلى سائق السيارة",
|
||||
"Ok I will go now.": "حسنًا، سأذهب الآن.",
|
||||
"Accepted Ride": "تم قبول الرحلة",
|
||||
"Driver Accepted the Ride for You": "الكابتن قبل رحلتك",
|
||||
"Promo": "عرض",
|
||||
"Show latest promo": "عرض آخر العروض",
|
||||
"Driver Accepted the Ride for You": "السائق قبل الرحلة لك",
|
||||
"Promo": "عرض ترويجي",
|
||||
"Show latest promo": "عرض أحدث عرض ترويجي",
|
||||
"Trip Monitoring": "مراقبة الرحلة",
|
||||
"Driver Is Going To Passenger": "الكابتن بالطريق إليك",
|
||||
"Please stay on the picked point.": "الرجاء البقاء في نقطة الانطلاق.",
|
||||
"message From Driver": "رسالة من الكابتن",
|
||||
"Driver Is Going To Passenger": "السائق في طريقه إليك",
|
||||
"Please stay on the picked point.":
|
||||
"يرجى البقاء في نقطة الالتقاط المحددة.",
|
||||
"message From Driver": "رسالة من السائق",
|
||||
"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": "هتدفع للسائق",
|
||||
"Don’t 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": "أهلاً بك في انطلق",
|
||||
"login or register subtitle":
|
||||
"أدخل رقم موبايلك لتسجيل الدخول أو لإنشاء حساب جديد",
|
||||
@@ -166,7 +184,10 @@ class MyTranslation extends Translations {
|
||||
"Add funds using our secure methods":
|
||||
"أضف رصيداً باستخدام طرقنا الآمنة",
|
||||
"Speed": "سرعة",
|
||||
"Comfort": "راحة",
|
||||
"Comfort": "مريحة",
|
||||
"Intaleq Balance": "رصيد انطلق",
|
||||
'Search for a starting point': 'ابحث عن نقطة انطلاق',
|
||||
'Top up Balance to continue': 'اشحن الرصيد للمتابعة',
|
||||
"Electric": "كهربائية",
|
||||
"Lady": "سيدة",
|
||||
"Van": "عائلية",
|
||||
|
||||
288
lib/main.dart
288
lib/main.dart
@@ -1,9 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:Intaleq/app_bindings.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/share_app_page.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_storage/get_storage.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:quick_actions/quick_actions.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'constant/api_key.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/translations.dart';
|
||||
import 'controller/payment/paymob/paymob_wallet.dart';
|
||||
import 'firebase_options.dart';
|
||||
import 'models/db_sql.dart';
|
||||
import 'splash_screen_page.dart';
|
||||
|
||||
// -- Global instances for easy access --
|
||||
final box = GetStorage();
|
||||
final storage = FlutterSecureStorage();
|
||||
// final PaymobPayment paymobPayment = PaymobPayment();
|
||||
// final PaymobPayment paymobPayment = PaymobPayment();
|
||||
// final PaymobPaymentWallet paymobPaymentWallet = PaymobPaymentWallet();
|
||||
|
||||
DbSql sql = DbSql.instance;
|
||||
// EncryptionHelper encryptionHelper = EncryptionHelper.instance;
|
||||
|
||||
// Firebase background message handler must be a top-level function.
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> backgroundMessageHandler(RemoteMessage message) async {
|
||||
await Firebase.initializeApp();
|
||||
|
||||
FirebaseMessagesController().fireBaseTitles(message);
|
||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
||||
print("Handling a background message: ${message.messageId}");
|
||||
}
|
||||
|
||||
void main() {
|
||||
// ⚠️ لا تستدعِ ensureInitialized هنا خارج الزون
|
||||
|
||||
runZonedGuarded(() async {
|
||||
// 1) أنشئ الـ Binding في نفس الـ Zone
|
||||
// Use runZonedGuarded to catch all unhandled exceptions in the app.
|
||||
runZonedGuarded<Future<void>>(() async {
|
||||
// --- Step 1: Critical initializations before runApp() ---
|
||||
// These must complete before the UI can be built.
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// 2) تهيئات خفيفة فقط قبل runApp
|
||||
await GetStorage.init();
|
||||
WakelockPlus.enable();
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
await Firebase.initializeApp(
|
||||
options: DefaultFirebaseOptions.currentPlatform);
|
||||
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
|
||||
await SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
}
|
||||
|
||||
// Stripe key initialization is very fast.
|
||||
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());
|
||||
|
||||
// 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) {
|
||||
// بديل آمن للتجميع
|
||||
// Global error handler.
|
||||
final s = error.toString();
|
||||
final ignored = s.contains('PERMISSION_DENIED') ||
|
||||
s.contains('FormatException') ||
|
||||
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 {
|
||||
const MyApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
|
||||
@override
|
||||
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(
|
||||
title: AppInformation.appName,
|
||||
@@ -304,19 +88,21 @@ class MyApp extends StatelessWidget {
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: localController.language,
|
||||
theme: localController.appTheme,
|
||||
key: UniqueKey(),
|
||||
|
||||
// routes: {'/':const HomePage()},
|
||||
// home: LoginCaptin());
|
||||
// --- [CRITICAL] ---
|
||||
// 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: '/',
|
||||
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: '/wallet', page: () => PassengerWallet()),
|
||||
GetPage(name: '/wallet', page: () => const PassengerWallet()),
|
||||
GetPage(name: '/profile', page: () => PassengerProfilePage()),
|
||||
GetPage(name: '/contactSupport', page: () => ContactUsPage()),
|
||||
],
|
||||
// home: SplashScreen()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class CarType {
|
||||
{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 = [
|
||||
CarType(
|
||||
carType: 'Speed',
|
||||
@@ -49,10 +49,6 @@ List<CarType> carTypes = [
|
||||
carType: 'Van',
|
||||
carDetail: 'Van for familly'.tr,
|
||||
image: 'assets/images/bus.png'),
|
||||
// CarType(
|
||||
// carType: 'Scooter',
|
||||
// carDetail: 'Delivery service'.tr,
|
||||
// image: 'assets/images/moto.png'),
|
||||
CarType(
|
||||
carType: 'Rayeh Gai',
|
||||
carDetail: "Best choice for cities".tr,
|
||||
@@ -65,6 +61,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
final textToSpeechController = Get.put(TextToSpeechController());
|
||||
|
||||
void _prepareCarTypes(MapPassengerController controller) {
|
||||
// This logic remains the same
|
||||
if (controller.distance > 33) {
|
||||
if (!carTypes.any((car) => car.carType == 'Rayeh Gai')) {
|
||||
carTypes.add(CarType(
|
||||
@@ -87,56 +84,69 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
controller.rideConfirm == false)) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// Added a BackdropFilter for a modern glassmorphism effect
|
||||
return Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(24),
|
||||
topRight: Radius.circular(24),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 5,
|
||||
),
|
||||
],
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildHeader(controller),
|
||||
const Divider(height: 1),
|
||||
_buildNegativeBalanceWarning(controller),
|
||||
SizedBox(
|
||||
height: 130,
|
||||
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);
|
||||
},
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor.withOpacity(0.9),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30),
|
||||
),
|
||||
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(
|
||||
BuildContext context, MapPassengerController controller) {
|
||||
@@ -145,32 +155,34 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _showPromoCodeDialog(context, controller),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||||
child: GestureDetector(
|
||||
onTap: () => _showPromoCodeDialog(context, controller),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
backgroundColor: AppColor.primaryColor.withOpacity(0.05),
|
||||
side: BorderSide(
|
||||
color: AppColor.primaryColor.withOpacity(0.7),
|
||||
width: 1.5,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
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),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.primaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: AppColor.primaryColor.withOpacity(0.5),
|
||||
width: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
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(
|
||||
context, controller, carType, textToSpeechController);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: 110,
|
||||
width: 120, // Increased width
|
||||
margin: const EdgeInsets.only(right: 12),
|
||||
padding: const EdgeInsets.all(8), // Added padding
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? AppColor.primaryColor.withOpacity(0.15)
|
||||
: AppColor.writeColor.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColor.primaryColor : Colors.transparent,
|
||||
width: 2.0,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: isSelected
|
||||
? [
|
||||
AppColor.primaryColor.withOpacity(0.3),
|
||||
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(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround, // Better alignment
|
||||
children: [
|
||||
Image.asset(carType.image, height: 50),
|
||||
const SizedBox(height: 8),
|
||||
Image.asset(carType.image, height: 55), // Slightly larger image
|
||||
Text(
|
||||
carType.carType.tr,
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(fontWeight: FontWeight.bold, fontSize: 14),
|
||||
.copyWith(fontWeight: FontWeight.bold, fontSize: 15),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_buildPriceDisplay(controller, carType),
|
||||
],
|
||||
),
|
||||
@@ -301,20 +333,44 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
|
||||
Widget _buildHeader(MapPassengerController controller) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 12),
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Choose your ride'.tr,
|
||||
style: AppStyle.headTitle.copyWith(fontSize: 22)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${controller.distance} ${'KM'.tr} • ${controller.hours > 0 ? '${controller.hours}h ${controller.minutes}m' : '${controller.minutes} min'}',
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
color: AppColor.primaryColor, fontWeight: FontWeight.bold),
|
||||
style: AppStyle.headTitle.copyWith(fontSize: 24)),
|
||||
const SizedBox(height: 8),
|
||||
// Added icons for better visual representation
|
||||
Row(
|
||||
children: [
|
||||
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;
|
||||
if (passengerWallet < 0.0) {
|
||||
return Container(
|
||||
color: AppColor.redColor.withOpacity(0.8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.redColor.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.white, size: 24),
|
||||
@@ -337,7 +397,8 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${'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(
|
||||
'${_getPassengerPriceText(carType, mapPassengerController)} ${'SYP'.tr}',
|
||||
style: AppStyle.subtitle.copyWith(
|
||||
fontSize: 14,
|
||||
fontSize: 15, // Increased font size
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColor.primaryColor));
|
||||
}
|
||||
|
||||
// UPDATED to include 'Electric'
|
||||
// --- LOGIC METHODS (UNCHANGED) ---
|
||||
|
||||
String _getPassengerPriceText(
|
||||
CarType carType, MapPassengerController mapPassengerController) {
|
||||
switch (carType.carType) {
|
||||
@@ -391,79 +453,97 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
Dialog(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(24.0)),
|
||||
backgroundColor: AppColor.secondaryColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(carType.image, height: 70),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
carType.carType.tr,
|
||||
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),
|
||||
),
|
||||
],
|
||||
backgroundColor:
|
||||
Colors.transparent, // Make dialog background transparent
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
// Main content container
|
||||
Container(
|
||||
margin:
|
||||
const EdgeInsets.only(top: 50), // Make space for the image
|
||||
padding: const EdgeInsets.fromLTRB(20, 70, 20, 20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColor.secondaryColor,
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_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(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColor.writeColor.withOpacity(0.8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
carType.carType.tr,
|
||||
style: AppStyle.headTitle.copyWith(fontSize: 24),
|
||||
),
|
||||
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),
|
||||
Expanded(
|
||||
child: MyElevatedButton(
|
||||
kolor: AppColor.greenColor,
|
||||
title: 'Next'.tr,
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
_handleCarSelection(
|
||||
context, mapPassengerController, carType);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor:
|
||||
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(
|
||||
MapPassengerController mapPassengerController, CarType carType) {
|
||||
switch (carType.carType) {
|
||||
@@ -553,7 +633,6 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// UPDATED to include 'Electric'
|
||||
double _getOriginalPrice(
|
||||
CarType carType, MapPassengerController mapPassengerController) {
|
||||
switch (carType.carType) {
|
||||
@@ -595,6 +674,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// --- BurcMoney Widget (Unchanged) ---
|
||||
class BurcMoney extends StatelessWidget {
|
||||
const BurcMoney({super.key});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.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 '../../../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() {
|
||||
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.addHome, 'addHome');
|
||||
}
|
||||
|
||||
return GetBuilder<MapPassengerController>(
|
||||
builder: (controller) => Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller.placeDestinationController,
|
||||
onChanged: (value) {
|
||||
if (controller.placeDestinationController.text.length > 2) {
|
||||
controller.getPlaces();
|
||||
controller.changeHeightPlaces();
|
||||
} else if (controller
|
||||
.placeDestinationController.text.isEmpty) {
|
||||
controller.clearPlacesDestination();
|
||||
controller.changeHeightPlaces();
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: controller.hintTextDestinationPoint,
|
||||
hintStyle:
|
||||
AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
||||
prefixIcon:
|
||||
const Icon(Icons.search, color: AppColor.primaryColor),
|
||||
suffixIcon: controller
|
||||
.placeDestinationController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
||||
onPressed: () {
|
||||
controller.placeDestinationController.clear();
|
||||
controller.clearPlacesDestination();
|
||||
controller.changeHeightPlaces();
|
||||
},
|
||||
)
|
||||
: 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],
|
||||
),
|
||||
id: 'destination_form', // Use an ID to allow targeted updates
|
||||
builder: (controller) {
|
||||
return Column(
|
||||
children: [
|
||||
// --- Widget for the search text field ---
|
||||
_SearchField(controller: controller),
|
||||
|
||||
// --- Widget for "Add Work" and "Add Home" buttons ---
|
||||
_QuickActions(
|
||||
controller: controller,
|
||||
addWorkValue: addWorkValue,
|
||||
addHomeValue: addHomeValue,
|
||||
),
|
||||
|
||||
// --- Widget for displaying search results, wrapped in its own GetBuilder ---
|
||||
_SearchResults(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------
|
||||
// -- Private Helper Widgets for Cleaner Code --
|
||||
// ---------------------------------------------------
|
||||
|
||||
/// A dedicated widget for the search input field.
|
||||
class _SearchField extends StatefulWidget {
|
||||
final MapPassengerController controller;
|
||||
|
||||
const _SearchField({required this.controller});
|
||||
|
||||
@override
|
||||
State<_SearchField> createState() => _SearchFieldState();
|
||||
}
|
||||
|
||||
class _SearchFieldState extends State<_SearchField> {
|
||||
Timer? _debounce;
|
||||
|
||||
// --- [إصلاح] Listener لتحديث الواجهة عند تغيير النص لإظهار/إخفاء زر المسح ---
|
||||
void _onTextChanged() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Add listener to update the suffix icon when text changes
|
||||
widget.controller.placeDestinationController.addListener(_onTextChanged);
|
||||
}
|
||||
|
||||
// --- [تحسين] إضافة Debouncer لتأخير البحث أثناء الكتابة ---
|
||||
void _onSearchChanged(String query) {
|
||||
if (_debounce?.isActive ?? false) _debounce!.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 500), () {
|
||||
if (query.length > 2) {
|
||||
widget.controller.getPlaces();
|
||||
widget.controller.changeHeightPlaces();
|
||||
} 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',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildQuickActionButton(
|
||||
icon: Icons.work_outline,
|
||||
text: box.read(BoxName.addWork) == 'addWork'
|
||||
? 'Add Work'.tr
|
||||
: 'To Work'.tr,
|
||||
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'),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 8.0),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
widget.controller.changeMainBottomMenuMap();
|
||||
widget.controller.changePickerShown();
|
||||
},
|
||||
icon: Icon(Icons.location_on_outlined,
|
||||
color: AppColor.accentColor, size: 30),
|
||||
tooltip: widget.controller.isAnotherOreder
|
||||
? 'Pick destination on map'
|
||||
: 'Pick on map',
|
||||
),
|
||||
),
|
||||
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),
|
||||
height: controller.placesDestination.isNotEmpty ? 300 : 0,
|
||||
decoration: BoxDecoration(
|
||||
@@ -137,20 +248,16 @@ GetBuilder<MapPassengerController> formSearchPlacesDestenation() {
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
physics: const ClampingScrollPhysics(),
|
||||
itemCount: controller.placesDestination.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const Divider(height: 1, color: Colors.grey),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var res = controller.placesDestination[index];
|
||||
|
||||
// استخراج البيانات حسب بنية السيرفر الجديد
|
||||
var title = res['name'] ?? 'Unknown Place';
|
||||
var address = res['address'] ?? 'Unknown Address';
|
||||
var latitude = res['latitude'];
|
||||
var longitude = res['longitude'];
|
||||
var primaryCategory =
|
||||
'Place'; // يمكن تطويره لاحقاً لو أضفت نوع للمكان
|
||||
final res = controller.placesDestination[index];
|
||||
final title = res['name_ar'] ?? res['name'] ?? 'Unknown Place';
|
||||
final address = res['address'] ?? 'Details not available';
|
||||
final latitude = res['latitude'];
|
||||
final longitude = res['longitude'];
|
||||
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.place, size: 30, color: Colors.grey),
|
||||
@@ -165,65 +272,105 @@ GetBuilder<MapPassengerController> formSearchPlacesDestenation() {
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.favorite_border, color: Colors.grey),
|
||||
onPressed: () 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,
|
||||
);
|
||||
}
|
||||
},
|
||||
onPressed: () => _handleAddToFavorites(
|
||||
context, latitude, longitude, title),
|
||||
),
|
||||
onTap: () async {
|
||||
if (latitude != null && longitude != null) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
onTap: () => _handlePlaceSelection(
|
||||
controller, latitude, longitude, title, index),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// --- [تحسين] استخراج المنطق المعقد إلى دوال مساعدة ---
|
||||
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({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
@@ -280,22 +427,31 @@ void _showChangeLocationDialog(
|
||||
|
||||
void _handleQuickAction(
|
||||
MapPassengerController controller, String boxName, String hintText) async {
|
||||
final latLng = LatLng(
|
||||
double.parse(box.read(boxName).toString().split(',')[0]),
|
||||
double.parse(box.read(boxName).toString().split(',')[1]),
|
||||
);
|
||||
controller.hintTextDestinationPoint = hintText;
|
||||
controller.changeMainBottomMenuMap();
|
||||
// --- [تحسين] قراءة وتحويل الإحداثيات بأمان أكبر ---
|
||||
try {
|
||||
final locationString = box.read(boxName).toString();
|
||||
final parts = locationString.split(',');
|
||||
final latLng = LatLng(
|
||||
double.parse(parts[0]),
|
||||
double.parse(parts[1]),
|
||||
);
|
||||
|
||||
await controller.getDirectionMap(
|
||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
||||
'${latLng.latitude},${latLng.longitude}',
|
||||
);
|
||||
controller.currentLocationToFormPlaces = false;
|
||||
controller.placesDestination = [];
|
||||
controller.clearPlacesStart();
|
||||
controller.clearPlacesDestination();
|
||||
controller.passengerStartLocationFromMap = false;
|
||||
controller.isPickerShown = false;
|
||||
controller.showBottomSheet1();
|
||||
controller.hintTextDestinationPoint = hintText;
|
||||
controller.changeMainBottomMenuMap();
|
||||
|
||||
await controller.getDirectionMap(
|
||||
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
|
||||
'${latLng.latitude},${latLng.longitude}',
|
||||
);
|
||||
|
||||
controller.currentLocationToFormPlaces = false;
|
||||
controller.clearPlacesDestination(); // Helper method in controller
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'package:flutter/material.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/style.dart';
|
||||
import '../../../controller/functions/toast.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../main.dart';
|
||||
|
||||
// ---------------------------------------------------
|
||||
// -- Widget for Start Point Search --
|
||||
// ---------------------------------------------------
|
||||
|
||||
GetBuilder<MapPassengerController> formSearchPlacesStart() {
|
||||
return GetBuilder<MapPassengerController>(
|
||||
@@ -14,70 +16,47 @@ GetBuilder<MapPassengerController> formSearchPlacesStart() {
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: controller.placeStartController,
|
||||
onChanged: (value) {
|
||||
if (controller.placeStartController.text.length > 5) {
|
||||
// Reduced character limit
|
||||
controller.getPlacesStart();
|
||||
controller.changeHeightStartPlaces();
|
||||
} else if (controller.placeStartController.text.isEmpty) {
|
||||
controller.clearPlacesStart();
|
||||
controller.changeHeightPlaces(); // Collapse if empty
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: controller.hintTextStartPoint,
|
||||
hintStyle:
|
||||
AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
||||
prefixIcon:
|
||||
const Icon(Icons.search, color: AppColor.primaryColor),
|
||||
suffixIcon: controller.placeStartController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
||||
onPressed: () {
|
||||
controller.placeStartController.clear();
|
||||
controller.clearPlacesStart();
|
||||
controller
|
||||
.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],
|
||||
),
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: controller.placeStartController,
|
||||
onChanged: (value) {
|
||||
if (controller.placeStartController.text.length > 2) {
|
||||
controller.getPlacesStart();
|
||||
} else if (controller.placeStartController.text.isEmpty) {
|
||||
controller.clearPlacesStart();
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search for a starting point'.tr,
|
||||
hintStyle: AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
||||
prefixIcon:
|
||||
const Icon(Icons.search, color: AppColor.primaryColor),
|
||||
suffixIcon: controller.placeStartController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
||||
onPressed: () {
|
||||
controller.placeStartController.clear();
|
||||
controller.clearPlacesStart();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
controller.startLocationFromMap = true;
|
||||
controller.changeMainBottomMenuMap();
|
||||
controller.changePickerShown();
|
||||
},
|
||||
icon: Icon(Icons.location_on_outlined,
|
||||
color: AppColor.accentColor, size: 30),
|
||||
tooltip: 'Choose on Map',
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide(color: AppColor.primaryColor),
|
||||
),
|
||||
],
|
||||
filled: true,
|
||||
fillColor: Colors.grey[50],
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
height: controller.placesStart.isNotEmpty ? controller.height : 0,
|
||||
height: controller.placesStart.isNotEmpty ? 300 : 0,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
@@ -85,48 +64,33 @@ GetBuilder<MapPassengerController> formSearchPlacesStart() {
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
physics: const ClampingScrollPhysics(),
|
||||
itemCount: controller.placesStart.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const Divider(height: 1, color: Colors.grey),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var res = controller.placesStart[index];
|
||||
var title = res['name_ar'] ?? res['name'] ?? 'Unknown Place';
|
||||
var address = res['address'] ?? 'Details not available';
|
||||
|
||||
return ListTile(
|
||||
leading: Image.network(res['icon'], width: 30, height: 30),
|
||||
title: Text(res['name'].toString(),
|
||||
leading: const Icon(Icons.place, size: 30, color: Colors.grey),
|
||||
title: Text(title,
|
||||
style: AppStyle.subtitle
|
||||
.copyWith(fontWeight: FontWeight.w500)),
|
||||
subtitle: Text(res['vicinity'].toString(),
|
||||
subtitle: Text(address,
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 12)),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.favorite_border, color: Colors.grey),
|
||||
onPressed: () async {
|
||||
await sql.insertMapLocation({
|
||||
'latitude': res['geometry']['location']['lat'],
|
||||
'longitude': res['geometry']['location']['lng'],
|
||||
'name': res['name'].toString(),
|
||||
'rate': res['rating'].toString(),
|
||||
}, TableName.placesFavorite);
|
||||
Toast.show(
|
||||
context,
|
||||
'${res['name']} ${'Saved 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();
|
||||
onTap: () {
|
||||
var latitude = res['latitude'];
|
||||
var longitude = res['longitude'];
|
||||
if (latitude != null && longitude != null) {
|
||||
controller.passengerLocation =
|
||||
LatLng(double.parse(latitude), double.parse(longitude));
|
||||
controller.placeStartController.text = title;
|
||||
controller.clearPlacesStart();
|
||||
// You might want to update the camera position on the map here
|
||||
controller.update();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import 'package:Intaleq/constant/box_name.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_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -6,9 +9,12 @@ import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'dart:ui'; // مهم لإضافة تأثير الضبابية
|
||||
|
||||
import '../../../constant/colors.dart';
|
||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
||||
import '../../../controller/functions/tts.dart';
|
||||
import '../../../controller/home/map_passenger_controller.dart';
|
||||
import '../../../controller/home/vip_waitting_page.dart';
|
||||
import '../../../env/env.dart';
|
||||
import '../../../print.dart';
|
||||
|
||||
// --- الدالة الرئيسية بالتصميم الجديد ---
|
||||
GetBuilder<MapPassengerController> leftMainMenuIcons() {
|
||||
@@ -70,11 +76,11 @@ GetBuilder<MapPassengerController> leftMainMenuIcons() {
|
||||
tooltip: 'VIP Waiting Page',
|
||||
onPressed: () => Get.to(() => VipWaittingPage()),
|
||||
),
|
||||
// _buildMapActionButton(
|
||||
// icon: Octicons.ellipsis,
|
||||
// tooltip: 'test',
|
||||
// onPressed: () => Get.to(() => TestPage()),
|
||||
// ),
|
||||
_buildMapActionButton(
|
||||
icon: Octicons.ellipsis,
|
||||
tooltip: 'test',
|
||||
onPressed: () => Get.to(() => TestPage()),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -118,15 +124,20 @@ class TestPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final firebaseMessagesController =
|
||||
Get.isRegistered<FirebaseMessagesController>()
|
||||
? Get.find<FirebaseMessagesController>()
|
||||
: Get.put(FirebaseMessagesController());
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
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(
|
||||
"Text Button",
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user