feat: refactor financial wallet UI components and add offline map service support
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user