feat: refactor financial wallet UI components and add offline map service support

This commit is contained in:
Hamza-Ayed
2026-04-21 00:35:30 +03:00
parent 4293d20561
commit b92db3bb39
99 changed files with 22888 additions and 27387 deletions

View File

@@ -1,15 +1,13 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:geolocator/geolocator.dart' as geo;
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:location/location.dart';
import 'package:battery_plus/battery_plus.dart';
import 'package:permission_handler/permission_handler.dart' as ph;
import 'package:sefer_driver/views/home/Captin/orderCaptin/order_request_page.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
import 'package:sefer_driver/constant/table_names.dart';
import 'package:trip_overlay_plugin/trip_overlay_plugin.dart';
@@ -17,6 +15,7 @@ import '../../constant/box_name.dart';
import '../../constant/links.dart';
import '../../main.dart';
import '../../print.dart';
import '../firebase/local_notification.dart';
import '../home/captin/home_captain_controller.dart';
import '../home/captin/map_driver_controller.dart';
import '../home/payment/captain_wallet_controller.dart';
@@ -63,6 +62,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
bool _isPowerSavingMode = false;
final List<Map<String, dynamic>> _trackBuffer = [];
final List<Map<String, dynamic>> _behaviorBuffer = [];
LatLng? _lastPosForDistance;
LatLng? _lastRecordedRealLoc;
@@ -137,26 +137,22 @@ class LocationController extends GetxController with WidgetsBindingObserver {
Log.print("📱 Lifecycle: App is in FOREGROUND");
box.write(BoxName.isAppInForeground, true);
// إيقاف خدمة الخلفية لأننا في الواجهة الآن
// إيقاف خدمة الخلفية
BackgroundServiceHelper.stopService();
// التأكد من أن السوكيت متصل، وإذا لا، نعيد الاتصال فوراً
if (socket == null || !socket!.connected) {
Log.print("🔄 Socket disconnected in background. Reconnecting now...");
if (socket == null || (!socket!.connected && !_isInitializingSocket)) {
Log.print("🔄 Initializing Socket on resume...");
initSocket();
} else {
// إذا كان متصلاً، ننعش المستمعين فقط للتأكد
_setupSocketListeners();
}
} else if (state == AppLifecycleState.paused ||
state == AppLifecycleState.detached) {
Log.print("📱 Lifecycle: App is in BACKGROUND");
box.write(BoxName.isAppInForeground, false);
// تشغيل خدمة الخلفية لضمان بقاء التطبيق حياً
BackgroundServiceHelper.startService();
// ملاحظة: لا نقطع السوكيت هنا، نتركه يعمل قدر الإمكان
// تشغيل خدمة الخلفية للأندرويد لضمان بقاء التطبيق حياً
if (!Platform.isIOS) {
BackgroundServiceHelper.startService();
}
}
}
@@ -179,69 +175,107 @@ class LocationController extends GetxController with WidgetsBindingObserver {
// ====== Socket Logic (Improved) ======
// ===================================================================
bool _isInitializingSocket = false;
void initSocket() {
// منع الاستدعاءات المتداخلة التي تسبب قتل الاتصال قبل اكتماله
if (_isInitializingSocket) {
Log.print("⏳ Socket is already initializing. Skipping redundant call.");
return;
}
if (socket != null && socket!.connected) {
Log.print("✅ Socket is already connected. No need to re-init.");
return;
}
String driverId = box.read(BoxName.driverID).toString();
String token = box.read(BoxName.tokenDriver).toString();
String platform = Platform.isIOS ? 'ios' : 'android';
// 1. إذا كان السوكيت موجوداً، فقط تأكد من اتصاله
_isInitializingSocket = true;
// تنظيف السوكيت القديم فقط إذا كان موجوداً وغير متصل
if (socket != null) {
if (!socket!.connected) {
Log.print("🟡 Socket exists but disconnected. Reconnecting...");
socket!.connect();
}
_setupSocketListeners(); // تحديث المستمعين
return;
Log.print("🧹 Cleaning up old socket instance...");
socket!.clearListeners();
socket!.dispose();
socket = null;
}
Log.print(
"🟡 [LocationController] Creating NEW Socket for Driver: $driverId");
"🟡 [LocationController] Initializing NEW Socket for Driver: $driverId");
// 2. إنشاء الاتصال
socket = IO.io(
'https://location.intaleq.xyz',
IO.OptionBuilder()
.setTransports(['websocket'])
.enableAutoConnect() // تفعيل إعادة الاتصال التلقائي
.setQuery(
{'driver_id': driverId, 'token': token, 'platform': platform})
.setReconnectionAttempts(double.infinity)
.setReconnectionDelay(2000)
.build());
try {
// العودة للـ Websocket حصراً لأنه الوحيد الذي ينجح في فتح القناة
socket = IO.io(
'https://location.intaleq.xyz',
IO.OptionBuilder()
.setTransports(['websocket'])
.setQuery({'driver_id': driverId, 'token': token, 'EIO': '3'})
.enableForceNew()
.build());
socket!.connect();
_setupSocketListeners();
_setupSocketListeners();
socket!.connect();
} catch (e) {
_isInitializingSocket = false;
Log.print("❌ Socket Initialization Exception: $e");
}
}
// دالة منفصلة لضمان عدم تكرار المستمعين
void _setupSocketListeners() {
if (socket == null) return;
// تنظيف القديم أولاً
socket!.off('connect');
socket!.off('disconnect');
socket!.off('new_ride_request');
socket!.off('ride_cancelled');
socket!.off('connect_error');
socket!.off('error');
socket!.onConnect((_) {
Log.print('✅ Socket Connected! ID: ${socket?.id}');
isSocketConnected = true;
_startHeartbeat();
_isInitializingSocket = false;
// ننتظر قليلاً للتأكد من تعبئة الـ IDs
Future.delayed(const Duration(milliseconds: 1000), () {
String? sid = socket?.id;
String? eid = socket?.io.engine?.id;
Log.print(
'✅ Socket Connected! ID: ${sid ?? eid ?? 'N/A'} (SID: $sid, EID: $eid)');
if (sid != null || eid != null) {
isSocketConnected = true;
_startHeartbeat();
}
});
});
socket!.onDisconnect((_) {
Log.print('❌ Socket Disconnected');
socket!.onDisconnect((data) {
_isInitializingSocket = false;
Log.print('❌ Socket Disconnected: $data');
isSocketConnected = false;
_stopHeartbeat();
});
socket!.onConnectError((err) {
_isInitializingSocket = false;
Log.print('❌ Socket Connect Error: $err');
});
socket!.onConnectTimeout((data) {
_isInitializingSocket = false;
Log.print('❌ Socket Connect Timeout: $data');
});
socket!.onError((err) {
_isInitializingSocket = false;
Log.print('❌ Socket General Error: $err');
});
socket!.on('reconnect_attempt', (attempt) {
Log.print('🔄 Socket Reconnecting... Attempt: $attempt');
});
// 🔥 الاستماع للطلبات الجديدة
socket!.on('new_ride_request', (data) {
Log.print("🔔 Socket: New Ride Request Arrived!");
@@ -296,8 +330,17 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (!isAppInForeground) {
Log.print(
"🛑 التطبيق في الخلفية. السوكيت سيتجاهل التوجيه ويترك المهمة للـ Overlay.");
return; // 👈 هذا السطر يمنع السوكيت من إكمال العمل وفتح الصفحة
"📱 [LocationController] Order received in background (iOS/Android). Source: $source");
if (Platform.isIOS) {
// على iOS، نقوم بإظهار إشعار محلي لأن الـ Overlay غير مدعوم
NotificationController().showNotification(
"طلب رحلة جديد 🚖",
"لديك طلب رحلة جديد، افتح التطبيق للموافقة عليه",
jsonEncode(rideData),
'ding.wav');
}
return;
}
try {
@@ -309,14 +352,23 @@ class LocationController extends GetxController with WidgetsBindingObserver {
// 3. تجهيز البيانات (DriverList)
List<dynamic> driverList = [];
if (rideData.length > 0) {
var sortedKeys = rideData.keys.map((e) => int.tryParse(e) ?? 0).toList()
..sort();
if (rideData.isNotEmpty) {
var sortedKeys = rideData.keys
.where((e) => int.tryParse(e) != null)
.map((e) => int.parse(e))
.toList()..sort();
for (var key in sortedKeys) {
driverList.add(rideData[key.toString()]);
}
}
// الحماية ضد البنية غير المكتملة
if (driverList.length <= 16) {
Log.print("❌ Socket Error: Parsed driver list is incomplete.");
return;
}
// 4. إغلاق النافذة (إن وجدت بالخطأ) والتنقل
try {
if (await TripOverlayPlugin.isOverlayActive()) {
@@ -459,21 +511,16 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_uploadBatchTimer?.cancel();
_socketHeartbeat?.cancel();
if (socket != null && socket!.connected) {
String driverId = box.read(BoxName.driverID).toString();
socket!.emit('update_location', {
'driver_id': driverId,
'lat': myLocation.latitude,
'lng': myLocation.longitude,
'heading': heading,
'speed': speed * 3.6,
'status': 'close', // Changed to off
'distance': totalDistance
});
socket!.disconnect();
if (socket != null) {
socket!.clearListeners();
socket!
.dispose(); // استخدام dispose بدلاً من disconnect لضمان تحرير الموارد على iOS
}
if (!Platform.isIOS) {
await BackgroundServiceHelper.stopService();
}
await BackgroundServiceHelper.stopService();
socket = null;
isSocketConnected = false;
_isReady = false;
@@ -526,16 +573,31 @@ class LocationController extends GetxController with WidgetsBindingObserver {
Future<void> _flushBufferToServer() async {
if (_trackBuffer.isEmpty) return;
List<Map<String, dynamic>> batch = List.from(_trackBuffer);
_trackBuffer.clear();
int itemsToTake = _trackBuffer.length > 100 ? 100 : _trackBuffer.length;
List<Map<String, dynamic>> batch = _trackBuffer.sublist(0, itemsToTake);
final String driverId = (box.read(BoxName.driverID) ?? '').toString();
try {
await CRUD().post(
var res = await CRUD().post(
link: '${AppLink.locationServer}/add_batch.php',
payload: {'driver_id': driverId, 'batch_data': jsonEncode(batch)},
);
if (res != 'failure') {
_trackBuffer.removeRange(0, itemsToTake);
} else {
_enforceBufferLimit();
}
} catch (e) {
Log.print('❌ Failed to upload batch: $e');
_enforceBufferLimit();
}
}
void _enforceBufferLimit() {
if (_trackBuffer.length > 500) {
_trackBuffer.removeRange(0, _trackBuffer.length - 500);
Log.print("⚠️ Buffer limit enforced. Removed oldest entries.");
}
}
@@ -547,32 +609,65 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (level >= powerSaveExitLevel) _isPowerSavingMode = false;
if (previousMode != _isPowerSavingMode) {
_startBatchTimers();
startLocationUpdates();
_updateLocationSettings();
}
});
}
Future<void> _updateLocationSettings() async {
if (_locSub == null) return;
int interval = _isPowerSavingMode ? 10000 : 5000;
try {
await location.changeSettings(
accuracy: LocationAccuracy.navigation,
interval: interval,
distanceFilter: _isPowerSavingMode ? 20 : 10,
);
Log.print("🔋 Location settings updated. Power Save: $_isPowerSavingMode");
} catch (e) {
Log.print("❌ Failed to update location settings: $e");
}
}
Future<void> _saveBehaviorIfMoved(LatLng pos, DateTime now,
{required double currentSpeed}) async {
final dist =
(_lastSqlLoc == null) ? 999.0 : _calculateDistance(_lastSqlLoc!, pos);
if (dist < 15.0) return;
final accel = _calcAcceleration(currentSpeed, now) ?? 0.0;
try {
await sql.insertData({
'driver_id': (box.read(BoxName.driverID) ?? '').toString(),
'latitude': pos.latitude,
'longitude': pos.longitude,
'acceleration': accel,
'created_at': now.toIso8601String(),
'updated_at': now.toIso8601String(),
}, TableName.behavior);
_lastSqlLoc = pos;
} catch (e) {
Log.print('SQLite Error: $e');
_lastSqlLoc = pos;
_behaviorBuffer.add({
'driver_id': (box.read(BoxName.driverID) ?? '').toString(),
'latitude': pos.latitude,
'longitude': pos.longitude,
'acceleration': accel,
'created_at': now.toIso8601String(),
'updated_at': now.toIso8601String(),
});
if (_behaviorBuffer.length >= 10) {
_flushBehaviorBuffer();
}
}
void _flushBehaviorBuffer() {
if (_behaviorBuffer.isEmpty) return;
List<Map<String, dynamic>> batch = List.from(_behaviorBuffer);
_behaviorBuffer.clear();
Future.microtask(() async {
try {
for (var data in batch) {
await sql.insertData(data, TableName.behavior);
}
} catch (e) {
Log.print('SQLite Batch Insert Error: $e');
}
});
}
// استبدال دالة Haversine اليدوية بـ Geolocator في باقي الكود أيضاً
// لأنها تعتمد على C++ في الأندرويد و Obj-C في الآيفون (Native Speed)
double _calculateDistance(LatLng a, LatLng b) {