25-10-5/1

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

View File

@@ -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) {

View File

@@ -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,
},

View File

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

View File

@@ -123,6 +123,7 @@ class CRUD {
// 401 → دع الطبقة العليا تتعامل مع التجديد
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') {

View File

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

View File

@@ -6,6 +6,7 @@ import 'dart:math' as math;
import 'dart:ui';
import 'dart: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);
});
// خذ أول 1015 للعرض (اختياري)، أو اعرض الكل
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

View File

@@ -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);
}
}

View File

@@ -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": "هتدفع للسائق",
"Dont forget your personal belongings.": "متنساش حاجاتك الشخصية.",
"Please make sure you have all your personal belongings and that any remaining fare, if applicable, has been added to your wallet before leaving. Thank you for choosing the Intaleq app":
"من فضلك تأكد إن معاك كل حاجاتك الشخصية وإن أي مبلغ متبقي، لو موجود، تم إضافته لمحفظتك قبل ما تمشي. شكرًا لاستخدامك تطبيق انطلق",
"Finish Monitor": "إنهاء المراقبة",
"Trip finished": "الرحلة انتهت",
"Call Income from Driver": "مكالمة واردة من السائق",
"Driver Cancelled Your Trip": "السائق ألغى رحلتك",
"you will pay to Driver you will be pay the cost of driver time look to your Intaleq Wallet":
"هتدفع للسائق هتدفع تكلفة وقت السائق شوف محفظة Intaleq بتاعتك",
"Order Applied": "تم تطبيق الطلب",
"welcome to intaleq": "أهلاً بك في انطلق",
"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": "عائلية",