590 lines
18 KiB
Dart
Executable File
590 lines
18 KiB
Dart
Executable File
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
|
import 'package:geolocator/geolocator.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:just_audio/just_audio.dart';
|
|
import 'dart:math' as math;
|
|
|
|
import '../../../constant/box_name.dart';
|
|
import '../../../constant/links.dart';
|
|
import '../../../main.dart';
|
|
import '../../../print.dart';
|
|
import '../../../views/home/Captin/driver_map_page.dart';
|
|
import '../../../views/home/Captin/orderCaptin/marker_generator.dart';
|
|
import '../../../views/widgets/mydialoug.dart';
|
|
import '../../functions/crud.dart';
|
|
import '../../functions/location_controller.dart';
|
|
import '../../home/captin/home_captain_controller.dart';
|
|
import '../../firebase/notification_service.dart';
|
|
import '../navigation/decode_polyline_isolate.dart';
|
|
|
|
class OrderRequestController extends GetxController
|
|
with WidgetsBindingObserver {
|
|
// --- متغيرات التايمر ---
|
|
double progress = 1.0;
|
|
int duration = 15;
|
|
int remainingTime = 15;
|
|
Timer? _timer;
|
|
|
|
bool applied = false;
|
|
final locationController = Get.put(LocationController());
|
|
|
|
// 🔥 متغير لمنع تكرار القبول
|
|
bool _isRideTakenHandled = false;
|
|
|
|
// --- الأيقونات والماركرز ---
|
|
BitmapDescriptor? driverIcon;
|
|
Map<MarkerId, Marker> markersMap = {};
|
|
Set<Marker> get markers => markersMap.values.toSet();
|
|
|
|
// --- البيانات والتحكم ---
|
|
// 🔥 تم إضافة myMapData لدعم السوكيت الجديد
|
|
List<dynamic>? myList;
|
|
Map<dynamic, dynamic>? myMapData;
|
|
|
|
GoogleMapController? mapController;
|
|
|
|
// الإحداثيات (أزلنا late لتجنب الأخطاء القاتلة)
|
|
double latPassenger = 0.0;
|
|
double lngPassenger = 0.0;
|
|
double latDestination = 0.0;
|
|
double lngDestination = 0.0;
|
|
|
|
// --- متغيرات العرض ---
|
|
String passengerRating = "5.0";
|
|
String tripType = "Standard";
|
|
String totalTripDistance = "--";
|
|
String totalTripDuration = "--";
|
|
String tripPrice = "--";
|
|
|
|
String timeToPassenger = "جاري الحساب...";
|
|
String distanceToPassenger = "--";
|
|
|
|
// --- الخريطة ---
|
|
Set<Polyline> polylines = {};
|
|
|
|
// حالة التطبيق والصوت
|
|
bool isInBackground = false;
|
|
final AudioPlayer audioPlayer = AudioPlayer();
|
|
|
|
@override
|
|
Future<void> onInit() async {
|
|
// 🛑 حماية من الفتح المتكرر لنفس الطلب
|
|
if (Get.arguments == null) {
|
|
print("❌ OrderController Error: No arguments received.");
|
|
Get.back(); // إغلاق الصفحة فوراً
|
|
return;
|
|
}
|
|
super.onInit();
|
|
WidgetsBinding.instance.addObserver(this);
|
|
|
|
_checkOverlay();
|
|
|
|
// 🔥 تهيئة البيانات هي الخطوة الأولى والأهم
|
|
_initializeData();
|
|
_parseExtraData();
|
|
|
|
// 1. تجهيز أيقونة السائق
|
|
await _prepareDriverIcon();
|
|
|
|
// 2. وضع الماركرز المبدئية
|
|
_updateMarkers(
|
|
paxTime: "...",
|
|
paxDist: "",
|
|
destTime: totalTripDuration,
|
|
destDist: totalTripDistance);
|
|
|
|
// 3. رسم مبدئي
|
|
_initialMapSetup();
|
|
|
|
// 4. الاستماع للسوكيت
|
|
_listenForRideTaken();
|
|
|
|
// 5. حساب المسارين
|
|
await _calculateFullJourney();
|
|
|
|
// 6. تشغيل التايمر
|
|
startTimer();
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 🔥🔥🔥 Smart Data Handling (List & Map Support) 🔥🔥🔥
|
|
// ----------------------------------------------------------------------
|
|
|
|
void _initializeData() {
|
|
var args = Get.arguments;
|
|
print("📦 Order Controller Received Type: ${args.runtimeType}");
|
|
print("📦 Order Controller Data: $args");
|
|
|
|
if (args != null) {
|
|
// الحالة 1: قائمة مباشرة (Legacy / Some Firebase formats)
|
|
if (args is List) {
|
|
myList = args;
|
|
}
|
|
// الحالة 2: خريطة (Map)
|
|
else if (args is Map) {
|
|
// أ) هل هي قادمة من Firebase وتحتوي على DriverList؟
|
|
if (args.containsKey('DriverList')) {
|
|
var listData = args['DriverList'];
|
|
if (listData is List) {
|
|
myList = listData;
|
|
} else if (listData is String) {
|
|
// أحياناً تصل كنص مشفر داخل الـ Map
|
|
try {
|
|
myList = jsonDecode(listData);
|
|
} catch (e) {
|
|
print("Error decoding DriverList: $e");
|
|
}
|
|
}
|
|
}
|
|
// ب) هل هي قادمة من Socket بالمفاتيح الرقمية ("0", "1", ...)؟
|
|
else {
|
|
myMapData = args;
|
|
}
|
|
}
|
|
}
|
|
|
|
// تعبئة الإحداثيات باستخدام الدالة الذكية _getValueAt
|
|
latPassenger = _parseCoord(_getValueAt(0));
|
|
lngPassenger = _parseCoord(_getValueAt(1));
|
|
latDestination = _parseCoord(_getValueAt(3));
|
|
lngDestination = _parseCoord(_getValueAt(4));
|
|
|
|
print(
|
|
"📍 Parsed Coordinates: Pax($latPassenger, $lngPassenger) -> Dest($latDestination, $lngDestination)");
|
|
}
|
|
|
|
/// 🔥 دالة ذكية تجلب القيمة سواء كانت البيانات في List أو Map
|
|
dynamic _getValueAt(int index) {
|
|
// الأولوية للقائمة
|
|
if (myList != null && index < myList!.length) {
|
|
return myList![index];
|
|
}
|
|
// ثم الخريطة (السوكيت) - المفاتيح عبارة عن String
|
|
if (myMapData != null && myMapData!.containsKey(index.toString())) {
|
|
return myMapData![index.toString()];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// الدالة التي يستخدمها باقي الكود لجلب البيانات كنصوص
|
|
String _safeGet(int index) {
|
|
var val = _getValueAt(index);
|
|
if (val != null) {
|
|
return val.toString();
|
|
}
|
|
return "";
|
|
}
|
|
|
|
double _parseCoord(dynamic val) {
|
|
if (val == null) return 0.0;
|
|
String s = val.toString().replaceAll(',', '').trim();
|
|
if (s.contains(' ')) s = s.split(' ')[0];
|
|
return double.tryParse(s) ?? 0.0;
|
|
}
|
|
|
|
void _parseExtraData() {
|
|
passengerRating = _safeGet(33).isEmpty ? "5.0" : _safeGet(33);
|
|
tripType = _safeGet(31);
|
|
totalTripDistance = _safeGet(5);
|
|
totalTripDuration = _safeGet(19);
|
|
tripPrice = _safeGet(2);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 🔥🔥🔥 Core Logic: Concurrent API Calls & Bounds 🔥🔥🔥
|
|
// ----------------------------------------------------------------------
|
|
|
|
Future<void> _calculateFullJourney() async {
|
|
try {
|
|
Position driverPos = await Geolocator.getCurrentPosition(
|
|
desiredAccuracy: LocationAccuracy.high);
|
|
LatLng driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
|
|
|
|
updateDriverLocation(driverLatLng, driverPos.heading);
|
|
|
|
var pickupFuture = _fetchRouteData(
|
|
start: driverLatLng,
|
|
end: LatLng(latPassenger, lngPassenger),
|
|
color: Colors.amber,
|
|
id: 'pickup_route');
|
|
|
|
var tripFuture = _fetchRouteData(
|
|
start: LatLng(latPassenger, lngPassenger),
|
|
end: LatLng(latDestination, lngDestination),
|
|
color: Colors.green,
|
|
id: 'trip_route',
|
|
isDashed: true);
|
|
|
|
var results = await Future.wait([pickupFuture, tripFuture]);
|
|
|
|
var pickupResult = results[0];
|
|
var tripResult = results[1];
|
|
|
|
if (pickupResult != null) {
|
|
distanceToPassenger = pickupResult['distance_text'];
|
|
timeToPassenger = pickupResult['duration_text'];
|
|
polylines.add(pickupResult['polyline']);
|
|
}
|
|
|
|
if (tripResult != null) {
|
|
totalTripDistance = tripResult['distance_text'];
|
|
totalTripDuration = tripResult['duration_text'];
|
|
polylines.add(tripResult['polyline']);
|
|
}
|
|
|
|
await _updateMarkers(
|
|
paxTime: timeToPassenger,
|
|
paxDist: distanceToPassenger,
|
|
destTime: totalTripDuration,
|
|
destDist: totalTripDistance);
|
|
|
|
zoomToFitRide(driverLatLng);
|
|
|
|
update();
|
|
} catch (e) {
|
|
print("❌ Error in Journey Calculation: $e");
|
|
}
|
|
}
|
|
|
|
Future<Map<String, dynamic>?> _fetchRouteData(
|
|
{required LatLng start,
|
|
required LatLng end,
|
|
required Color color,
|
|
required String id,
|
|
bool isDashed = false}) async {
|
|
try {
|
|
// حماية من الإحداثيات الصفرية
|
|
if (start.latitude == 0 || end.latitude == 0) return null;
|
|
|
|
String apiUrl = "https://routesjo.intaleq.xyz/route/v1/driving";
|
|
String coords =
|
|
"${start.longitude},${start.latitude};${end.longitude},${end.latitude}";
|
|
String url = "$apiUrl/$coords?steps=false&overview=full";
|
|
|
|
var response = await http.get(Uri.parse(url));
|
|
if (response.statusCode == 200) {
|
|
var json = jsonDecode(response.body);
|
|
if (json['code'] == 'Ok' && json['routes'].isNotEmpty) {
|
|
var route = json['routes'][0];
|
|
|
|
double distM = double.parse(route['distance'].toString());
|
|
double durS = double.parse(route['duration'].toString());
|
|
|
|
String distText = "${(distM / 1000).toStringAsFixed(1)} كم";
|
|
String durText = "${(durS / 60).toStringAsFixed(0)} دقيقة";
|
|
|
|
String geometry = route['geometry'];
|
|
List<LatLng> points = await compute(decodePolylineIsolate, geometry);
|
|
|
|
Polyline polyline = Polyline(
|
|
polylineId: PolylineId(id),
|
|
color: color,
|
|
width: 5,
|
|
points: points,
|
|
patterns:
|
|
isDashed ? [PatternItem.dash(10), PatternItem.gap(5)] : [],
|
|
startCap: Cap.roundCap,
|
|
endCap: Cap.roundCap,
|
|
);
|
|
|
|
return {
|
|
'distance_text': distText,
|
|
'duration_text': durText,
|
|
'polyline': polyline
|
|
};
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print("Route Fetch Error: $e");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void zoomToFitRide(LatLng driverPos) {
|
|
if (mapController == null) return;
|
|
|
|
// حماية من النقاط الصفرية
|
|
if (latPassenger == 0 || latDestination == 0) return;
|
|
|
|
List<LatLng> points = [
|
|
driverPos,
|
|
LatLng(latPassenger, lngPassenger),
|
|
LatLng(latDestination, lngDestination),
|
|
];
|
|
|
|
double minLat = points.first.latitude;
|
|
double maxLat = points.first.latitude;
|
|
double minLng = points.first.longitude;
|
|
double maxLng = points.first.longitude;
|
|
|
|
for (var p in points) {
|
|
if (p.latitude < minLat) minLat = p.latitude;
|
|
if (p.latitude > maxLat) maxLat = p.latitude;
|
|
if (p.longitude < minLng) minLng = p.longitude;
|
|
if (p.longitude > maxLng) maxLng = p.longitude;
|
|
}
|
|
|
|
mapController!.animateCamera(CameraUpdate.newLatLngBounds(
|
|
LatLngBounds(
|
|
southwest: LatLng(minLat, minLng),
|
|
northeast: LatLng(maxLat, maxLng),
|
|
),
|
|
100.0,
|
|
));
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Markers & Setup
|
|
// ----------------------------------------------------------------------
|
|
|
|
Future<void> _prepareDriverIcon() async {
|
|
driverIcon = await MarkerGenerator.createDriverMarker();
|
|
}
|
|
|
|
Future<void> _updateMarkers(
|
|
{required String paxTime,
|
|
required String paxDist,
|
|
String? destTime,
|
|
String? destDist}) async {
|
|
// حماية إذا لم يتم جلب الإحداثيات
|
|
if (latPassenger == 0 || latDestination == 0) return;
|
|
|
|
final BitmapDescriptor pickupIcon =
|
|
await MarkerGenerator.createCustomMarkerBitmap(
|
|
title: paxTime,
|
|
subtitle: paxDist,
|
|
color: Colors.green.shade700,
|
|
iconData: Icons.person_pin_circle,
|
|
);
|
|
|
|
final BitmapDescriptor dropoffIcon =
|
|
await MarkerGenerator.createCustomMarkerBitmap(
|
|
title: destTime ?? totalTripDuration,
|
|
subtitle: destDist ?? totalTripDistance,
|
|
color: Colors.red.shade800,
|
|
iconData: Icons.flag,
|
|
);
|
|
|
|
markersMap[const MarkerId('pax')] = Marker(
|
|
markerId: const MarkerId('pax'),
|
|
position: LatLng(latPassenger, lngPassenger),
|
|
icon: pickupIcon,
|
|
anchor: const Offset(0.5, 0.85),
|
|
);
|
|
|
|
markersMap[const MarkerId('dest')] = Marker(
|
|
markerId: const MarkerId('dest'),
|
|
position: LatLng(latDestination, lngDestination),
|
|
icon: dropoffIcon,
|
|
anchor: const Offset(0.5, 0.85),
|
|
);
|
|
|
|
update();
|
|
}
|
|
|
|
void _initialMapSetup() async {
|
|
Position driverPos = await Geolocator.getCurrentPosition();
|
|
LatLng driverLatLng = LatLng(driverPos.latitude, driverPos.longitude);
|
|
|
|
if (driverIcon != null) {
|
|
markersMap[const MarkerId('driver')] = Marker(
|
|
markerId: const MarkerId('driver'),
|
|
position: driverLatLng,
|
|
icon: driverIcon!,
|
|
rotation: driverPos.heading,
|
|
anchor: const Offset(0.5, 0.5),
|
|
flat: true,
|
|
zIndex: 10);
|
|
}
|
|
|
|
if (latPassenger != 0 && lngPassenger != 0) {
|
|
polylines.add(Polyline(
|
|
polylineId: const PolylineId('temp_line'),
|
|
points: [driverLatLng, LatLng(latPassenger, lngPassenger)],
|
|
color: Colors.grey,
|
|
width: 2,
|
|
patterns: [PatternItem.dash(10), PatternItem.gap(10)],
|
|
));
|
|
|
|
zoomToFitRide(driverLatLng);
|
|
}
|
|
|
|
update();
|
|
}
|
|
|
|
void updateDriverLocation(LatLng newPos, double heading) {
|
|
if (driverIcon != null) {
|
|
markersMap[const MarkerId('driver')] = Marker(
|
|
markerId: const MarkerId('driver'),
|
|
position: newPos,
|
|
icon: driverIcon!,
|
|
rotation: heading,
|
|
anchor: const Offset(0.5, 0.5),
|
|
flat: true,
|
|
zIndex: 10,
|
|
);
|
|
update();
|
|
}
|
|
}
|
|
|
|
void onMapCreated(GoogleMapController controller) {
|
|
mapController = controller;
|
|
}
|
|
|
|
// --- قبول الطلب وإدارة التايمر ---
|
|
void startTimer() {
|
|
_timer?.cancel();
|
|
remainingTime = duration;
|
|
_playAudio();
|
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
if (remainingTime <= 0) {
|
|
timer.cancel();
|
|
_stopAudio();
|
|
if (!applied) Get.back();
|
|
} else {
|
|
remainingTime--;
|
|
progress = remainingTime / duration;
|
|
update();
|
|
}
|
|
});
|
|
}
|
|
|
|
void endTimer() => _timer?.cancel();
|
|
void changeApplied() => applied = true;
|
|
|
|
void _playAudio() async {
|
|
try {
|
|
await audioPlayer.setAsset('assets/order.mp3', preload: true);
|
|
await audioPlayer.setLoopMode(LoopMode.one);
|
|
await audioPlayer.play();
|
|
} catch (e) {
|
|
print(e);
|
|
}
|
|
}
|
|
|
|
void _stopAudio() => audioPlayer.stop();
|
|
|
|
void _listenForRideTaken() {
|
|
if (locationController.socket != null) {
|
|
locationController.socket!.off('ride_taken');
|
|
locationController.socket!.on('ride_taken', (data) {
|
|
if (_isRideTakenHandled) return;
|
|
String takenRideId = data['ride_id'].toString();
|
|
String myCurrentRideId = _safeGet(16);
|
|
String whoTookIt = data['taken_by_driver_id'].toString();
|
|
String myDriverId = box.read(BoxName.driverID).toString();
|
|
|
|
if (takenRideId == myCurrentRideId && whoTookIt != myDriverId) {
|
|
_isRideTakenHandled = true;
|
|
endTimer();
|
|
if (Get.isSnackbarOpen) Get.closeCurrentSnackbar();
|
|
if (Get.isDialogOpen ?? false) Get.back();
|
|
Get.back();
|
|
Get.snackbar("تنبيه", "تم قبول الطلب من قبل سائق آخر",
|
|
backgroundColor: Colors.orange, colorText: Colors.white);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Lifecycle
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
super.didChangeAppLifecycleState(state);
|
|
if (state == AppLifecycleState.paused ||
|
|
state == AppLifecycleState.detached) {
|
|
isInBackground = true;
|
|
} else if (state == AppLifecycleState.resumed) {
|
|
isInBackground = false;
|
|
FlutterOverlayWindow.closeOverlay();
|
|
}
|
|
}
|
|
|
|
void _checkOverlay() async {
|
|
if (Platform.isAndroid && await FlutterOverlayWindow.isActive()) {
|
|
await FlutterOverlayWindow.closeOverlay();
|
|
}
|
|
}
|
|
|
|
// Accept Order Logic
|
|
Future<void> acceptOrder() async {
|
|
endTimer();
|
|
_stopAudio();
|
|
|
|
var res = await CRUD()
|
|
.post(link: "${AppLink.ride}/rides/acceptRide.php", payload: {
|
|
'id': _safeGet(16),
|
|
'rideTimeStart': DateTime.now().toString(),
|
|
'status': 'Apply',
|
|
'passengerToken': _safeGet(9),
|
|
'driver_id': box.read(BoxName.driverID),
|
|
});
|
|
|
|
if (res == 'failure') {
|
|
MyDialog().getDialog("عذراً، الطلب أخذه سائق آخر.", '', () {
|
|
Get.back();
|
|
Get.back();
|
|
});
|
|
} else {
|
|
Get.put(HomeCaptainController()).changeRideId();
|
|
box.write(BoxName.statusDriverLocation, 'on');
|
|
changeApplied();
|
|
|
|
var rideArgs = {
|
|
'passengerLocation': '${_safeGet(0)},${_safeGet(1)}',
|
|
'passengerDestination': '${_safeGet(3)},${_safeGet(4)}',
|
|
'Duration': totalTripDuration,
|
|
'totalCost': _safeGet(26),
|
|
'Distance': totalTripDistance,
|
|
'name': _safeGet(8),
|
|
'phone': _safeGet(10),
|
|
'email': _safeGet(28),
|
|
'WalletChecked': _safeGet(13),
|
|
'tokenPassenger': _safeGet(9),
|
|
'direction':
|
|
'https://www.google.com/maps/dir/${_safeGet(0)}/${_safeGet(1)}/',
|
|
'DurationToPassenger': timeToPassenger,
|
|
'rideId': _safeGet(16),
|
|
'passengerId': _safeGet(7),
|
|
'driverId': _safeGet(18),
|
|
'durationOfRideValue': totalTripDuration,
|
|
'paymentAmount': _safeGet(2),
|
|
'paymentMethod': _safeGet(13) == 'true' ? 'visa' : 'cash',
|
|
'isHaveSteps': _safeGet(20),
|
|
'step0': _safeGet(21),
|
|
'step1': _safeGet(22),
|
|
'step2': _safeGet(23),
|
|
'step3': _safeGet(24),
|
|
'step4': _safeGet(25),
|
|
'passengerWalletBurc': _safeGet(26),
|
|
'timeOfOrder': DateTime.now().toString(),
|
|
'totalPassenger': _safeGet(2),
|
|
'carType': _safeGet(31),
|
|
'kazan': _safeGet(32),
|
|
'startNameLocation': _safeGet(29),
|
|
'endNameLocation': _safeGet(30),
|
|
};
|
|
|
|
box.write(BoxName.rideArguments, rideArgs);
|
|
Get.off(() => PassengerLocationMapPage(), arguments: rideArgs);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
locationController.socket?.off('ride_taken');
|
|
audioPlayer.dispose();
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
_timer?.cancel();
|
|
mapController?.dispose();
|
|
super.onClose();
|
|
}
|
|
}
|