Fixes & Updates - 2026-06-01: Integrate Back-End v3 updates, fix call/connection issues across apps
This commit is contained in:
92
lib/controller/functions/app_update_controller.dart
Normal file
92
lib/controller/functions/app_update_controller.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../constant/info.dart';
|
||||
import '../../constant/links.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../print.dart';
|
||||
import 'crud.dart';
|
||||
|
||||
class AppUpdateController extends GetxController {
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// الفحص التلقائي عند التشغيل لتحديثات المتجر
|
||||
checkSmartUpdate();
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// الدالة الذكية المدمجة (الآن تفحص المتجر فقط لأن Shorebird يعمل تلقائياً بالخلفية)
|
||||
// ======================================================================
|
||||
Future<void> checkSmartUpdate() async {
|
||||
Log.print("🔄 بدء فحص تحديثات المتجر...");
|
||||
|
||||
// 1. فحص تحديث المتجر (Native Update)
|
||||
await _checkStoreUpdate();
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// 1. تحديث المتجر الأساسي
|
||||
// ======================================================================
|
||||
Future<bool> _checkStoreUpdate() async {
|
||||
try {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final currentBuildNumber = packageInfo.buildNumber;
|
||||
|
||||
// استخدام نفس الـ Endpoint والمعايير الموجودة في التطبيق
|
||||
var response = await CRUD().get(link: AppLink.packageInfo, payload: {
|
||||
"platform": Platform.isAndroid ? 'android' : 'ios',
|
||||
"appName": AppInformation.appVersion,
|
||||
});
|
||||
|
||||
if (response != 'failure') {
|
||||
var decoded = jsonDecode(response);
|
||||
if (decoded['status'] == 'success' && decoded['message'] != null && decoded['message'].isNotEmpty) {
|
||||
String latestBuildNumber = decoded['message'][0]['version'].toString();
|
||||
|
||||
// مقارنة الـ Build Number
|
||||
if (latestBuildNumber != currentBuildNumber) {
|
||||
_showStoreUpdateDialog();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print("❌ Store update check error: $e");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
// دوال مساعدة
|
||||
// ======================================================================
|
||||
|
||||
void _showStoreUpdateDialog() {
|
||||
final String storeUrl = Platform.isAndroid
|
||||
? 'https://play.google.com/store/apps/details?id=com.intaleq_driver'
|
||||
: 'https://apps.apple.com/jo/app/intaleq-driver/id6482995159';
|
||||
|
||||
Get.defaultDialog(
|
||||
title: "تحديث جديد متوفر".tr,
|
||||
middleText: "يوجد إصدار جديد من التطبيق في المتجر، يرجى التحديث للحصول على الميزات الجديدة.".tr,
|
||||
barrierDismissible: false,
|
||||
onWillPop: () async => false,
|
||||
confirm: ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColor.primaryColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))
|
||||
),
|
||||
onPressed: () async {
|
||||
if (await canLaunchUrl(Uri.parse(storeUrl))) {
|
||||
await launchUrl(Uri.parse(storeUrl), mode: LaunchMode.externalApplication);
|
||||
}
|
||||
},
|
||||
child: Text("تحديث الآن".tr, style: const TextStyle(color: Colors.white)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
80
lib/controller/functions/audio_recorder_controller.dart
Normal file
80
lib/controller/functions/audio_recorder_controller.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'dart:io';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:record/record.dart';
|
||||
|
||||
class AudioRecorderController extends GetxController {
|
||||
AudioPlayer audioPlayer = AudioPlayer();
|
||||
AudioRecorder recorder = AudioRecorder();
|
||||
|
||||
bool isRecording = false;
|
||||
bool isPlaying = false;
|
||||
bool isPaused = false;
|
||||
String filePath = '';
|
||||
String? selectedFilePath;
|
||||
double currentPosition = 0;
|
||||
double totalDuration = 0;
|
||||
|
||||
// Start recording
|
||||
Future<void> startRecording({String? rideId}) async {
|
||||
final bool isPermissionGranted = await recorder.hasPermission();
|
||||
if (!isPermissionGranted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final String dateStr =
|
||||
'${DateTime.now().year}-${DateTime.now().month.toString().padLeft(2, '0')}-${DateTime.now().day.toString().padLeft(2, '0')}';
|
||||
// Generate a unique file name
|
||||
String fileName = (rideId != null && rideId.isNotEmpty && rideId != 'yet' && rideId != 'null')
|
||||
? '${dateStr}_$rideId.m4a'
|
||||
: '$dateStr.m4a';
|
||||
filePath = '${directory.path}/$fileName';
|
||||
|
||||
const config = RecordConfig(
|
||||
encoder: AudioEncoder.aacLc,
|
||||
sampleRate: 44100,
|
||||
bitRate: 128000,
|
||||
);
|
||||
|
||||
await recorder.start(config, path: filePath);
|
||||
|
||||
isRecording = true;
|
||||
update();
|
||||
}
|
||||
|
||||
// Stop recording
|
||||
Future<void> stopRecording() async {
|
||||
await recorder.stop();
|
||||
isRecording = false;
|
||||
isPaused = false;
|
||||
update();
|
||||
}
|
||||
|
||||
// Get a list of recorded files
|
||||
Future<List<String>> getRecordedFiles() async {
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final files = await directory.list().toList();
|
||||
return files
|
||||
.map((file) => file.path)
|
||||
.where((path) => path.endsWith('.m4a'))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Delete a specific recorded file
|
||||
Future<void> deleteRecordedFile(String filePath) async {
|
||||
final file = File(filePath);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
audioPlayer.dispose();
|
||||
recorder.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'dart:io';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||
|
||||
void showInBrowser(String url) async {
|
||||
if (await canLaunchUrl(Uri.parse(url))) {
|
||||
@@ -7,34 +9,42 @@ void showInBrowser(String url) async {
|
||||
} else {}
|
||||
}
|
||||
|
||||
Future<void> makePhoneCall(String phoneNumber) async {
|
||||
String cleanAndFormatPhoneNumber(String phoneNumber) {
|
||||
// 1. Clean the number
|
||||
String formattedNumber = phoneNumber.replaceAll(RegExp(r'\s+'), '');
|
||||
|
||||
// 2. Format logic (Syria/International)
|
||||
// 2. Format logic (Syria/Egypt/International)
|
||||
if (formattedNumber.length > 6) {
|
||||
if (formattedNumber.startsWith('09')) {
|
||||
formattedNumber = '+963${formattedNumber.substring(1)}';
|
||||
} else if (formattedNumber.startsWith('01') && formattedNumber.length == 11) {
|
||||
formattedNumber = '+20${formattedNumber.substring(1)}';
|
||||
} else if (formattedNumber.startsWith('00')) {
|
||||
formattedNumber = '+${formattedNumber.substring(2)}';
|
||||
} else if (!formattedNumber.startsWith('+')) {
|
||||
formattedNumber = '+$formattedNumber';
|
||||
}
|
||||
}
|
||||
return formattedNumber;
|
||||
}
|
||||
|
||||
// 3. Create URI
|
||||
final Uri launchUri = Uri(
|
||||
scheme: 'tel',
|
||||
path: formattedNumber,
|
||||
);
|
||||
Future<void> makePhoneCall(String phoneNumber) async {
|
||||
String formattedNumber = cleanAndFormatPhoneNumber(phoneNumber);
|
||||
|
||||
if (!formattedNumber.startsWith('+963')) {
|
||||
mySnackeBarError("Calling non-Syrian numbers is not supported".tr);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create URI directly from String to avoid double encoding '+' as '%2B'
|
||||
final Uri launchUri = Uri.parse('tel:$formattedNumber');
|
||||
|
||||
// 4. Execute with externalApplication mode
|
||||
try {
|
||||
// Attempt to launch directly without checking canLaunchUrl first
|
||||
// (Sometimes canLaunchUrl returns false on some devices even if it works)
|
||||
if (!await launchUrl(launchUri, mode: LaunchMode.externalApplication)) {
|
||||
throw 'Could not launch $launchUri';
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback: Try checking canLaunchUrl if the direct launch fails
|
||||
if (await canLaunchUrl(launchUri)) {
|
||||
await launchUrl(launchUri);
|
||||
} else {
|
||||
@@ -45,23 +55,30 @@ Future<void> makePhoneCall(String phoneNumber) async {
|
||||
|
||||
void launchCommunication(
|
||||
String method, String contactInfo, String message) async {
|
||||
String formattedContact = cleanAndFormatPhoneNumber(contactInfo);
|
||||
// WhatsApp prefers the phone number without the '+' prefix
|
||||
String whatsappContact = formattedContact.replaceAll('+', '');
|
||||
String url;
|
||||
|
||||
if (Platform.isIOS) {
|
||||
switch (method) {
|
||||
case 'phone':
|
||||
url = 'tel:$contactInfo';
|
||||
if (!formattedContact.startsWith('+963')) {
|
||||
mySnackeBarError("Calling non-Syrian numbers is not supported".tr);
|
||||
return;
|
||||
}
|
||||
url = 'tel:$formattedContact';
|
||||
break;
|
||||
case 'sms':
|
||||
url = 'sms:$contactInfo?body=${Uri.encodeComponent(message)}';
|
||||
url = 'sms:$formattedContact?body=${Uri.encodeComponent(message)}';
|
||||
break;
|
||||
case 'whatsapp':
|
||||
url =
|
||||
'https://api.whatsapp.com/send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
|
||||
'https://api.whatsapp.com/send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
|
||||
break;
|
||||
case 'email':
|
||||
url =
|
||||
'mailto:$contactInfo?subject=Subject&body=${Uri.encodeComponent(message)}';
|
||||
'mailto:$formattedContact?subject=Subject&body=${Uri.encodeComponent(message)}';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
@@ -69,27 +86,29 @@ void launchCommunication(
|
||||
} else if (Platform.isAndroid) {
|
||||
switch (method) {
|
||||
case 'phone':
|
||||
url = 'tel:$contactInfo';
|
||||
if (!formattedContact.startsWith('+963')) {
|
||||
mySnackeBarError("Calling non-Syrian numbers is not supported".tr);
|
||||
return;
|
||||
}
|
||||
url = 'tel:$formattedContact';
|
||||
break;
|
||||
case 'sms':
|
||||
url = 'sms:$contactInfo?body=${Uri.encodeComponent(message)}';
|
||||
url = 'sms:$formattedContact?body=${Uri.encodeComponent(message)}';
|
||||
break;
|
||||
case 'whatsapp':
|
||||
// Check if WhatsApp is installed
|
||||
final bool whatsappInstalled =
|
||||
await canLaunchUrl(Uri.parse('whatsapp://'));
|
||||
if (whatsappInstalled) {
|
||||
url =
|
||||
'whatsapp://send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
|
||||
'whatsapp://send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
|
||||
} else {
|
||||
// Provide an alternative action, such as opening the WhatsApp Web API
|
||||
url =
|
||||
'https://api.whatsapp.com/send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
|
||||
'https://api.whatsapp.com/send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
|
||||
}
|
||||
break;
|
||||
case 'email':
|
||||
url =
|
||||
'mailto:$contactInfo?subject=Subject&body=${Uri.encodeComponent(message)}';
|
||||
'mailto:$formattedContact?subject=Subject&body=${Uri.encodeComponent(message)}';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
|
||||
@@ -54,8 +54,11 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
late final HomeCaptainController _homeCtrl;
|
||||
late final CaptainWalletController _walletCtrl;
|
||||
|
||||
LatLng myLocation = const LatLng(0, 0);
|
||||
double heading = 0.0;
|
||||
LatLng myLocation = LatLng(
|
||||
box.read('last_lat') ?? 0.0,
|
||||
box.read('last_lng') ?? 0.0,
|
||||
);
|
||||
double heading = box.read('last_heading') ?? 0.0;
|
||||
double speed = 0.0;
|
||||
double totalDistance = 0.0;
|
||||
bool _isReady = false;
|
||||
@@ -379,7 +382,23 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
Log.print("Overlay check error: $e");
|
||||
}
|
||||
|
||||
if (Get.currentRoute != '/OrderRequestPage') {
|
||||
// 🔥 [Fix Active-Ride Guard] منع فتح صفحة الطلبات أثناء وجود السائق في رحلة نشطة
|
||||
// هذا يمنع socket event جديد من تعطيل رحلة جارية
|
||||
String? currentRideStatus = box.read(BoxName.rideStatus);
|
||||
bool hasActiveRide = (currentRideStatus == 'Begin' ||
|
||||
currentRideStatus == 'Apply' ||
|
||||
currentRideStatus == 'Arrived');
|
||||
String currentRoute = Get.currentRoute;
|
||||
bool isOnMapPage = currentRoute.contains('MapPage') ||
|
||||
currentRoute.contains('PassengerLocation');
|
||||
|
||||
if (hasActiveRide || isOnMapPage) {
|
||||
Log.print(
|
||||
"⛔ [LocationController] Ignoring new ride request — driver has active ride ($currentRideStatus) or is on map page ($currentRoute).");
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentRoute != '/OrderRequestPage') {
|
||||
Log.print("🚀 Socket: Navigating to OrderRequestPage...");
|
||||
Get.toNamed('/OrderRequestPage', arguments: {
|
||||
'myListString': jsonEncode(driverList),
|
||||
@@ -398,6 +417,10 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
void _startHeartbeat() {
|
||||
_socketHeartbeat?.cancel();
|
||||
_socketHeartbeat = Timer.periodic(const Duration(seconds: 25), (timer) {
|
||||
// [Fix 6] تخطي الإرسال إذا كان stream الموقع نشطاً.
|
||||
// الـ _locSub يرسل update_location عند كل تحرك (كل 5-10 ثوانٍ) تلقائياً.
|
||||
// الـ heartbeat يكون مفيداً فقط عندما يتوقف الـ stream (الجهاز ثابت أو أوقف الخدمة).
|
||||
if (_locSub != null) return;
|
||||
if (socket != null && isSocketConnected && myLocation.latitude != 0) {
|
||||
emitLocationToSocket(myLocation, heading, speed);
|
||||
}
|
||||
@@ -491,6 +514,10 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
speed = loc.speed ?? 0.0;
|
||||
heading = loc.heading ?? 0.0;
|
||||
|
||||
box.write('last_lat', pos.latitude);
|
||||
box.write('last_lng', pos.longitude);
|
||||
box.write('last_heading', heading);
|
||||
|
||||
if (_lastPosForDistance != null) {
|
||||
final d = _calculateDistance(_lastPosForDistance!, pos);
|
||||
if (d > 5.0) totalDistance += d;
|
||||
@@ -499,10 +526,25 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
|
||||
update();
|
||||
emitLocationToSocket(pos, heading, speed);
|
||||
|
||||
if (Get.isRegistered<HomeCaptainController>()) {
|
||||
final homeCtrl = Get.find<HomeCaptainController>();
|
||||
if (homeCtrl.isActive &&
|
||||
homeCtrl.mapHomeCaptainController != null &&
|
||||
homeCtrl.isHomeMapActive &&
|
||||
homeCtrl.isMapReadyForCommands) {
|
||||
homeCtrl.mapHomeCaptainController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(pos, 17.5),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await _saveBehaviorIfMoved(pos, now, currentSpeed: speed);
|
||||
}, onError: (e) => Log.print('❌ Location Stream Error: $e'));
|
||||
}
|
||||
|
||||
Timer? _socketWatchdogTimer;
|
||||
|
||||
Future<void> stopLocationUpdates() async {
|
||||
Log.print("🛑 Stopping Location Updates...");
|
||||
|
||||
@@ -511,11 +553,11 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
_recordTimer?.cancel();
|
||||
_uploadBatchTimer?.cancel();
|
||||
_socketHeartbeat?.cancel();
|
||||
_socketWatchdogTimer?.cancel();
|
||||
|
||||
if (socket != null) {
|
||||
socket!.clearListeners();
|
||||
socket!
|
||||
.dispose(); // استخدام dispose بدلاً من disconnect لضمان تحرير الموارد على iOS
|
||||
socket!.dispose();
|
||||
}
|
||||
|
||||
if (!Platform.isIOS) {
|
||||
@@ -534,6 +576,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
void _startBatchTimers() {
|
||||
_recordTimer?.cancel();
|
||||
_uploadBatchTimer?.cancel();
|
||||
_socketWatchdogTimer?.cancel();
|
||||
|
||||
final recDur =
|
||||
_isPowerSavingMode ? recordIntervalPowerSave : recordIntervalNormal;
|
||||
@@ -544,6 +587,14 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
_recordTimer =
|
||||
Timer.periodic(recDur, (_) => _recordCurrentLocationToBuffer());
|
||||
_uploadBatchTimer = Timer.periodic(upDur, (_) => _flushBufferToServer());
|
||||
|
||||
// محاولة إعادة الاتصال بالسوكيت إذا انقطع كل 3 ثواني
|
||||
_socketWatchdogTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||||
if (!isSocketConnected && !_isInitializingSocket) {
|
||||
Log.print("🔄 Socket Watchdog: Attempting to reconnect socket...");
|
||||
initSocket();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _recordCurrentLocationToBuffer() {
|
||||
@@ -736,7 +787,30 @@ class LocationController extends GetxController with WidgetsBindingObserver {
|
||||
Future<LocationData?> getLocation() async {
|
||||
try {
|
||||
if (await _ensureServiceAndPermission()) {
|
||||
return await location.getLocation();
|
||||
final locData = await location.getLocation();
|
||||
if (locData != null && locData.latitude != null && locData.longitude != null) {
|
||||
myLocation = LatLng(locData.latitude!, locData.longitude!);
|
||||
heading = locData.heading ?? 0.0;
|
||||
speed = locData.speed ?? 0.0;
|
||||
|
||||
box.write('last_lat', myLocation.latitude);
|
||||
box.write('last_lng', myLocation.longitude);
|
||||
box.write('last_heading', heading);
|
||||
|
||||
update();
|
||||
|
||||
if (Get.isRegistered<HomeCaptainController>()) {
|
||||
final homeCtrl = Get.find<HomeCaptainController>();
|
||||
if (homeCtrl.mapHomeCaptainController != null &&
|
||||
homeCtrl.isMapReadyForCommands) {
|
||||
Log.print("📍 [LocationController] Animating camera to single location update");
|
||||
homeCtrl.mapHomeCaptainController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(myLocation, 17.5),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return locData;
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('❌ FAILED to get single location: $e');
|
||||
|
||||
Reference in New Issue
Block a user