Compare commits
13 Commits
f08ee61a7e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38f7abcd59 | ||
|
|
12c5ce31a2 | ||
|
|
fbfde115a8 | ||
|
|
3c0ae4cf2f | ||
|
|
374f9e9bf3 | ||
|
|
9b1008a0bf | ||
|
|
b1b8efdd7d | ||
|
|
a69e4c6912 | ||
|
|
14484fcd8f | ||
|
|
ea55a25af2 | ||
|
|
482c1296bc | ||
|
|
1cc66029a3 | ||
|
|
95fb065bdb |
5
.env
@@ -3,9 +3,10 @@ basicCompareFaces=zjujluqfpj:nqrYjp@1737XrXlBl
|
|||||||
basicCompareFacesURL='https://face-detect-f6924392c4c7.herokuapp.com/compare_faces'
|
basicCompareFacesURL='https://face-detect-f6924392c4c7.herokuapp.com/compare_faces'
|
||||||
accountSIDTwillo=QFx0qy456juj383n9xuy2194q629q1fj0y7XrXlBl
|
accountSIDTwillo=QFx0qy456juj383n9xuy2194q629q1fj0y7XrXlBl
|
||||||
serverAPI=QQQQobSrrFi:QVQ87xU7zwCvmZzZdaxuS2f23Y4mz7MzyOzr8od2br6KYyeFaTVLG3K3hx5ZaUyx7eYvAYpAVdKk-286NTRi3zs9iSOnXtXRIxswg3KecBmsl3VxJ9wO-vIpwu4Pv7dkHkXniuxMSDgWXrXlBl
|
serverAPI=QQQQobSrrFi:QVQ87xU7zwCvmZzZdaxuS2f23Y4mz7MzyOzr8od2br6KYyeFaTVLG3K3hx5ZaUyx7eYvAYpAVdKk-286NTRi3zs9iSOnXtXRIxswg3KecBmsl3VxJ9wO-vIpwu4Pv7dkHkXniuxMSDgWXrXlBl
|
||||||
mapAPIKEY=AIzaSyCFsWBqvkXzk1Gb-bCGxwqTwJQKIeHjH64
|
mapAPIKEY=AIzaSyAPFR_XbRN0XZ5Iz3AYDjNYHGJG2s2QWwM
|
||||||
email=@intaleqapp.com
|
email=@intaleqapp.com
|
||||||
mapAPIKEYIOS=AIzaSyDzGO9a-1IDMLD2FxhmOO9ONL1gMssFa9g
|
mapKeyOsm=maldev@route-dollars
|
||||||
|
mapAPIKEYIOS=AIzaSyDdqkLMCrqjVrn7XmadIqynyoBa7P27OeM
|
||||||
twilloRecoveryCode=CAU79DHPH1BjE9PUH4ETXTSXZXrXlBl
|
twilloRecoveryCode=CAU79DHPH1BjE9PUH4ETXTSXZXrXlBl
|
||||||
apiKeyHere=g_WNUb5L-tripz7-F8omHpUmgIzH7ETeH9xZ8RwGG9_G8zX9A
|
apiKeyHere=g_WNUb5L-tripz7-F8omHpUmgIzH7ETeH9xZ8RwGG9_G8zX9A
|
||||||
authTokenTwillo=70u98ju0214oxx4q0u74028u021u4qu65XrXlBl
|
authTokenTwillo=70u98ju0214oxx4q0u74028u021u4qu65XrXlBl
|
||||||
|
|||||||
@@ -7,28 +7,28 @@ plugins {
|
|||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
id "dev.flutter.flutter-gradle-plugin"
|
id "dev.flutter.flutter-gradle-plugin"
|
||||||
}
|
}
|
||||||
|
|
||||||
def keystoreProperties = new Properties()
|
def keystoreProperties = new Properties()
|
||||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||||
// Load keystore properties if the file exists
|
// Load keystore properties if the file exists
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.intaleq_driver"
|
namespace = "com.intaleq_driver"
|
||||||
compileSdk = 35
|
compileSdk = 36
|
||||||
// ndkVersion = flutter.ndkVersion
|
ndkVersion "29.0.14033849"
|
||||||
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
path "src/main/cpp/CMakeLists.txt"
|
path "src/main/cpp/CMakeLists.txt"
|
||||||
version "3.31.5" // Match cmake_minimum_required in CMakeLists.txt
|
// Using a common, stable version. Your CMakeLists.txt requests 3.10.2,
|
||||||
|
// but your old gradle file had 3.22.1 and 3.31.5. Let's use a stable one.
|
||||||
|
version "3.22.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
ndk {
|
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" // Keep these!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
@@ -39,16 +39,18 @@ android {
|
|||||||
jvmTarget = JavaVersion.VERSION_1_8
|
jvmTarget = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merged the two defaultConfig sections into one. This is the correct way.
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
|
||||||
applicationId = "com.intaleq_driver"
|
applicationId = "com.intaleq_driver"
|
||||||
// You can update the following values to match your application needs.
|
minSdkVersion = 23
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
|
||||||
minSdk = 29
|
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 14
|
versionCode = 57
|
||||||
versionName = '1.0.14'
|
versionName = '1.1.57' // I've used the higher version name
|
||||||
multiDexEnabled =true
|
multiDexEnabled = true
|
||||||
|
|
||||||
|
ndk {
|
||||||
|
abiFilters "armeabi-v7a", "arm64-v8a" // Keep these!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -59,13 +61,13 @@ android {
|
|||||||
storePassword keystoreProperties['storePassword']
|
storePassword keystoreProperties['storePassword']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
signingConfig signingConfigs.release
|
signingConfig signingConfigs.release
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,11 +75,12 @@ android {
|
|||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
implementation 'com.scottyab:rootbeer-lib:0.1.0'
|
implementation 'com.scottyab:rootbeer-lib:0.1.0'
|
||||||
implementation 'com.stripe:paymentsheet:20.52.2'
|
implementation 'com.stripe:paymentsheet:20.52.2'
|
||||||
|
implementation "androidx.concurrent:concurrent-futures:1.2.0" // Added this from your first file, it was missing
|
||||||
implementation 'com.google.android.gms:play-services-safetynet:18.0.1'
|
implementation 'com.google.android.gms:play-services-safetynet:18.0.1'
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- ===== Permissions ===== -->
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
@@ -27,73 +28,75 @@
|
|||||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||||
<uses-permission android:name="android.permission.QUICKBOOT_POWERON" />
|
<uses-permission android:name="android.permission.QUICKBOOT_POWERON" />
|
||||||
<uses-permission android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
<uses-permission android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||||
|
<uses-permission android:name="android.permission.PICTURE_IN_PICTURE" />
|
||||||
<uses-feature android:name="android.hardware.camera" />
|
<uses-feature android:name="android.hardware.camera" />
|
||||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||||
|
<application android:name="${applicationName}" android:icon="@mipmap/launcher_icon"
|
||||||
<application
|
android:label="@string/label" android:enableOnBackInvokedCallback="true"
|
||||||
android:name="${applicationName}"
|
android:allowBackup="false" android:fullBackupContent="false"
|
||||||
android:icon="@mipmap/launcher_icon"
|
|
||||||
android:label="Intaleq Driver"
|
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:allowBackup="false"
|
|
||||||
android:fullBackupContent="false"
|
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:usesCleartextTraffic="false"
|
android:usesCleartextTraffic="false" android:theme="@style/LaunchTheme">
|
||||||
android:theme="@style/LaunchTheme">
|
<!-- Flutter embedding v2 -->
|
||||||
|
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||||
<activity
|
<!-- تحديد نقطة دخول خلفية (للـ overlay / background executor) -->
|
||||||
android:name=".MainActivity"
|
<!-- <meta-data
|
||||||
|
android:name="io.flutter.embedding.android.BackgroundExecutor.DART_ENTRYPOINT"
|
||||||
|
android:value="overlayMain" />
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.BackgroundExecutor.DART_LIBRARY_URI"
|
||||||
|
android:value="main.dart" /> -->
|
||||||
|
<!-- خرائط + إشعارات فFirebase (قناة افتراضية) -->
|
||||||
|
<meta-data android:name="com.google.android.geo.API_KEY" android:value="@string/api_key" />
|
||||||
|
<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
|
android:value="@string/default_notification_channel_id" />
|
||||||
|
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />
|
||||||
|
<!-- Main Activity -->
|
||||||
|
<activity android:name=".MainActivity"
|
||||||
android:configChanges="orientation|keyboardHidden|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:exported="true"
|
android:exported="true" android:hardwareAccelerated="true" android:launchMode="singleTask"
|
||||||
android:hardwareAccelerated="true"
|
android:theme="@style/LaunchTheme" android:windowSoftInputMode="adjustResize">
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:theme="@style/LaunchTheme"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
|
||||||
<meta-data android:name="io.flutter.embedding.android.NormalTheme"
|
<meta-data android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme" />
|
android:resource="@style/NormalTheme" />
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- Deep Link: intaleqapp://... -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<!-- Define your custom scheme -->
|
|
||||||
<data android:scheme="intaleqapp" />
|
<data android:scheme="intaleqapp" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- أنشطة ومكوّنات إضافية -->
|
||||||
<activity
|
<activity android:name="com.yalantis.ucrop.UCropActivity" android:screenOrientation="portrait"
|
||||||
android:name="com.yalantis.ucrop.UCropActivity"
|
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
|
android:theme="@style/Theme.AppCompat.Light.NoActionBar" />
|
||||||
|
<!-- خدماتك الخاصة -->
|
||||||
<meta-data android:name="com.google.android.geo.API_KEY" android:value="@string/api_key" />
|
<service
|
||||||
<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id"
|
android:name="id.flutter.flutter_background_service.BackgroundService"
|
||||||
android:value="@string/default_notification_channel_id" />
|
android:foregroundServiceType="location"
|
||||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
/>
|
||||||
<service android:name=".MyFirebaseMessagingService" android:exported="false" />
|
<service android:name=".MyFirebaseMessagingService" android:exported="false" />
|
||||||
<service android:name=".LocationUpdatesService" android:exported="false"
|
<service android:name=".LocationUpdatesService" android:exported="false"
|
||||||
android:foregroundServiceType="location" />
|
android:foregroundServiceType="location" />
|
||||||
|
<!-- خدمة Firebase الرسمية لاستقبال رسائل FCM -->
|
||||||
<service android:name="com.google.firebase.messaging.FirebaseMessagingService"
|
<service android:name="com.google.firebase.messaging.FirebaseMessagingService"
|
||||||
android:exported="false" tools:replace="android:exported">
|
android:exported="false" tools:replace="android:exported">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
<service android:name="com.phan_tech.flutter_overlay_apps.OverlayService"
|
<!-- خدمة overlay للمكتبة الأولى (إن كنت تستخدمها) -->
|
||||||
android:exported="false" />
|
<!-- <service
|
||||||
|
android:name="com.phan_tech.flutter_overlay_apps.OverlayService"
|
||||||
|
android:exported="false" /> -->
|
||||||
<service android:name="flutter.overlay.window.flutter_overlay_window.OverlayService"
|
<service android:name="flutter.overlay.window.flutter_overlay_window.OverlayService"
|
||||||
android:foregroundServiceType="specialUse">
|
android:exported="false" android:foregroundServiceType="specialUse" />
|
||||||
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
<!-- خدمة overlay الخاصة بمكتبة flutter_overlay_window -->
|
||||||
android:value="explanation_for_special_use" />
|
<!-- استقبال توكن/رسائل قديمة (توافقية) -->
|
||||||
</service>
|
|
||||||
|
|
||||||
<receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
|
<receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver"
|
||||||
android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
|
android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -101,18 +104,16 @@
|
|||||||
<category android:name="com.intaleq_driver" />
|
<category android:name="com.intaleq_driver" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<!-- خدمة الفقاعة الخاصة بك -->
|
||||||
<service android:name="com.dsaved.bubblehead.bubble.BubbleHeadService"
|
<service android:name="com.dsaved.bubblehead.bubble.BubbleHeadService" android:enabled="true"
|
||||||
android:enabled="true"
|
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="intent.bring.app.to.foreground" />
|
<action android:name="intent.bring.app.to.foreground" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
<!-- Notif schedulers -->
|
||||||
<receiver
|
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
|
||||||
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
|
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<receiver
|
<receiver
|
||||||
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
|
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
|
||||||
@@ -124,8 +125,8 @@
|
|||||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
<!-- مستقبل برودكاست خاص بك -->
|
||||||
<receiver android:name=".YourBroadcastReceiver" android:exported="false" />
|
<receiver android:name=".YourBroadcastReceiver" android:exported="false" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,6 +1,8 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="security_warning_title">تحذير أمني</string>
|
<string name="security_warning_title">تحذير أمني</string>
|
||||||
<string name="security_warning_message">تم اكتشاف مشكلة أمنية أو تعديل على هذا الجهاز. لا يمكن تشغيل التطبيق على هذا الجهاز.</string>
|
<string name="security_warning_message">تم اكتشاف مشكلة أمنية أو تعديل على هذا الجهاز. لا يمكن
|
||||||
|
تشغيل التطبيق على هذا الجهاز.</string>
|
||||||
<string name="exit_button">إغلاق التطبيق</string>
|
<string name="exit_button">إغلاق التطبيق</string>
|
||||||
<string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string>
|
<string name="device_secure">الجهاز آمن. الاستمرار بشكل طبيعي.</string>
|
||||||
|
<string name="label">انطلق درايفر</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -3,13 +3,19 @@
|
|||||||
<!-- <string name="default_notification_channel_id">ride_channel</string> -->
|
<!-- <string name="default_notification_channel_id">ride_channel</string> -->
|
||||||
<!-- <string name="default_notification_channel_id">default_channel</string> -->
|
<!-- <string name="default_notification_channel_id">default_channel</string> -->
|
||||||
<string name="default_notification_channel_id">high_importance_channel</string>
|
<string name="default_notification_channel_id">high_importance_channel</string>
|
||||||
<string name="api_key">AIzaSyCFsWBqvkXzk1Gb-bCGxwqTwJQKIeHjH64</string>
|
<string name="api_key">AIzaSyACAeqD8qnNYwHKj1qRec6F3AKzdo__CiQ</string>
|
||||||
<string name="security_warning_title">Security Warning</string>
|
<string name="security_warning_title">Security Warning</string>
|
||||||
<string name="api_key_safety">AIzaSyB04YNW3LbvmQ5lX1t2bOwEU18-KUoovzw</string>
|
<string name="api_key_safety">AIzaSyB04YNW3LbvmQ5lX1t2bOwEU18-KUoovzw</string>
|
||||||
|
<string name="label">Intaleq Driver</string>
|
||||||
|
|
||||||
<string name="security_warning_message">A security issue or modification has been detected on
|
<string name="security_warning_message">A security issue or modification has been detected on
|
||||||
this device. The app cannot run on this device.</string>
|
this device. The app cannot run on this device.</string>
|
||||||
<string name="exit_button">Exit App</string>
|
<string name="exit_button">Exit App</string>
|
||||||
<string name="device_secure">Device is secure. Proceeding normally.</string>
|
<string name="device_secure">Device is secure. Proceeding normally.</string>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <string name="default_notification_channel_id">driver_service_channel</string> -->
|
||||||
|
<string name="location_service_channel_id">location_service_channel</string>
|
||||||
|
<string name="geolocator_channel_id">geolocator_channel</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -6,3 +6,4 @@ android.nonTransitiveRClass=true
|
|||||||
android.nonFinalResIds=true
|
android.nonFinalResIds=true
|
||||||
dart.obfuscation=true
|
dart.obfuscation=true
|
||||||
android.enableR8.fullMode=true
|
android.enableR8.fullMode=true
|
||||||
|
org.gradle.java.home=/Library/Java/JavaVirtualMachines/temurin-17.jdk/Contents/Home
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 461 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 76 KiB |
BIN
assets/images/logo1.png
Executable file → Normal file
|
Before Width: | Height: | Size: 715 KiB After Width: | Height: | Size: 149 KiB |
BIN
assets/images/shamcashsend.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
@@ -21,7 +21,7 @@ class Bubble {
|
|||||||
/// puts app in background and shows floaty-bubble head
|
/// puts app in background and shows floaty-bubble head
|
||||||
Future<void> startBubbleHead({bool sendAppToBackground = true}) async {
|
Future<void> startBubbleHead({bool sendAppToBackground = true}) async {
|
||||||
ByteData bytes = await rootBundle.load(
|
ByteData bytes = await rootBundle.load(
|
||||||
'assets/images/s.png',
|
'assets/images/logo1.png',
|
||||||
);
|
);
|
||||||
var buffer = bytes.buffer;
|
var buffer = bytes.buffer;
|
||||||
var encodedImage = base64.encode(Uint8List.view(buffer));
|
var encodedImage = base64.encode(Uint8List.view(buffer));
|
||||||
|
|||||||
24
collect_code.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# اسم الملف الناتج
|
||||||
|
output_filename = "full_project_code_driver.txt"
|
||||||
|
# المجلد الذي تريد سحب الكود منه
|
||||||
|
source_folder = "./lib"
|
||||||
|
|
||||||
|
with open(output_filename, "w", encoding="utf-8") as outfile:
|
||||||
|
for root, dirs, files in os.walk(source_folder):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith(".dart"):
|
||||||
|
file_path = os.path.join(root, file)
|
||||||
|
# كتابة فاصل واسم الملف ليعرف الذكاء الاصطناعي أين يبدأ الملف
|
||||||
|
outfile.write(f"\n\n{'='*50}\n")
|
||||||
|
outfile.write(f"FILE PATH: {file_path}\n")
|
||||||
|
outfile.write(f"{'='*50}\n\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, "r", encoding="utf-8") as infile:
|
||||||
|
outfile.write(infile.read())
|
||||||
|
except Exception as e:
|
||||||
|
outfile.write(f"Error reading file: {e}\n")
|
||||||
|
|
||||||
|
print(f"تم تجميع الكود بنجاح في الملف: {output_filename}")
|
||||||
80436
full_project_code_driver.txt
Normal file
@@ -6,11 +6,14 @@ class BoxName {
|
|||||||
static const String initializationVector = 'initializationVector';
|
static const String initializationVector = 'initializationVector';
|
||||||
static const String firstTimeLoadKey = 'firstTimeLoadKey';
|
static const String firstTimeLoadKey = 'firstTimeLoadKey';
|
||||||
static const String jwt = "jwt";
|
static const String jwt = "jwt";
|
||||||
|
static const String blockUntilDate = "blockUntilDate";
|
||||||
static const String rideId = "rideId";
|
static const String rideId = "rideId";
|
||||||
static const String rideArgumentsFromBackground =
|
static const String rideArgumentsFromBackground =
|
||||||
"rideArgumentsFromBackground";
|
"rideArgumentsFromBackground";
|
||||||
static const String FCM_PRIVATE_KEY = "FCM_PRIVATE_KEY";
|
static const String FCM_PRIVATE_KEY = "FCM_PRIVATE_KEY";
|
||||||
static const String hmac = "hmac";
|
static const String hmac = "hmac";
|
||||||
|
static const String ttsEnabled = "ttsEnabled";
|
||||||
|
static const String rideType = "rideType";
|
||||||
static const String walletType = "walletType";
|
static const String walletType = "walletType";
|
||||||
static const String fingerPrint = "fingerPrint";
|
static const String fingerPrint = "fingerPrint";
|
||||||
static const String updateInterval = "updateInterval";
|
static const String updateInterval = "updateInterval";
|
||||||
@@ -36,6 +39,7 @@ class BoxName {
|
|||||||
static const String phoneVerified = "phoneVerified";
|
static const String phoneVerified = "phoneVerified";
|
||||||
static const String carPlate = "carPlate";
|
static const String carPlate = "carPlate";
|
||||||
static const String statusDriverLocation = "statusDriverLocation";
|
static const String statusDriverLocation = "statusDriverLocation";
|
||||||
|
static const String isAppInForeground = "isAppInForeground";
|
||||||
static const String rideStatus = "rideStatus";
|
static const String rideStatus = "rideStatus";
|
||||||
static const String nameArabic = "nameArabic";
|
static const String nameArabic = "nameArabic";
|
||||||
static const String carYear = "carYear";
|
static const String carYear = "carYear";
|
||||||
|
|||||||
107
lib/constant/country_polygons.dart
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// في ملف: constant/country_polygons.dart
|
||||||
|
|
||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
|
||||||
|
class CountryPolygons {
|
||||||
|
// ==========================================================
|
||||||
|
// 1. الأردن: تغطية الممر الحضري الرئيسي (من إربد شمالاً حتى العقبة جنوباً)
|
||||||
|
// حوالي 12 نقطة
|
||||||
|
// ==========================================================
|
||||||
|
static final List<LatLng> jordanBoundary = [
|
||||||
|
// شمال إربد (قرب الحدود)
|
||||||
|
const LatLng(32.65, 35.80),
|
||||||
|
// شمال شرق المفرق
|
||||||
|
const LatLng(32.35, 37.00),
|
||||||
|
// شرق الزرقاء / الأزرق
|
||||||
|
const LatLng(31.85, 36.80),
|
||||||
|
// جنوب شرق (نهاية الزحف السكاني)
|
||||||
|
const LatLng(31.00, 36.50),
|
||||||
|
// جنوب / معان
|
||||||
|
const LatLng(30.30, 35.75),
|
||||||
|
// العقبة
|
||||||
|
const LatLng(29.50, 35.00),
|
||||||
|
// البحر الأحمر / الحدود الغربية
|
||||||
|
const LatLng(29.50, 34.85),
|
||||||
|
// غرب وادي عربة
|
||||||
|
const LatLng(30.80, 35.25),
|
||||||
|
// منطقة البحر الميت / السلط
|
||||||
|
const LatLng(32.00, 35.50),
|
||||||
|
// العودة عبر وادي الأردن إلى الشمال
|
||||||
|
const LatLng(32.45, 35.60),
|
||||||
|
// العودة لنقطة إربد
|
||||||
|
const LatLng(32.65, 35.80),
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// 2. سوريا: تغطية الممر الغربي والساحلي (درعا، دمشق، حمص، حماة، حلب، الساحل)
|
||||||
|
// حوالي 14 نقطة
|
||||||
|
// ==========================================================
|
||||||
|
static final List<LatLng> syriaBoundary = [
|
||||||
|
// درعا / الجنوب
|
||||||
|
const LatLng(32.65, 35.95),
|
||||||
|
// شرق السويداء (حدود المنطقة المأهولة)
|
||||||
|
const LatLng(32.85, 37.10),
|
||||||
|
// أطراف دمشق الشرقية
|
||||||
|
const LatLng(33.50, 36.65),
|
||||||
|
// تدمر (أقصى امتداد شرقي للمضلع)
|
||||||
|
const LatLng(34.50, 38.30),
|
||||||
|
// الرقة (شمال شرق)
|
||||||
|
const LatLng(35.95, 38.80),
|
||||||
|
// حلب (الشمال)
|
||||||
|
const LatLng(36.45, 37.15),
|
||||||
|
// الحدود الشمالية الغربية (إدلب / تركيا)
|
||||||
|
const LatLng(36.50, 36.50),
|
||||||
|
// اللاذقية (الساحل)
|
||||||
|
const LatLng(35.50, 35.75),
|
||||||
|
// طرطوس (الساحل)
|
||||||
|
const LatLng(34.80, 35.85),
|
||||||
|
// حمص
|
||||||
|
const LatLng(34.70, 36.70),
|
||||||
|
// حماة
|
||||||
|
const LatLng(35.10, 36.70),
|
||||||
|
// العودة إلى منطقة دمشق
|
||||||
|
const LatLng(33.40, 36.30),
|
||||||
|
// العودة إلى درعا
|
||||||
|
const LatLng(32.65, 35.95),
|
||||||
|
];
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// 3. مصر: تغطية القاهرة الكبرى، الدلتا، والإسكندرية والإسماعيلية
|
||||||
|
// حوالي 10 نقاط
|
||||||
|
// ==========================================================
|
||||||
|
static final List<LatLng> egyptBoundary = [
|
||||||
|
// جنوب الفيوم (أقصى امتداد جنوبي غربي)
|
||||||
|
const LatLng(29.20, 30.60),
|
||||||
|
// جنوب القاهرة (العياط)
|
||||||
|
const LatLng(29.80, 31.30),
|
||||||
|
// شرق السويس
|
||||||
|
const LatLng(29.95, 32.70),
|
||||||
|
// الإسماعيلية / القناة
|
||||||
|
const LatLng(30.60, 32.25),
|
||||||
|
// بورسعيد / أطراف الدلتا الشمالية الشرقية
|
||||||
|
const LatLng(31.30, 31.80),
|
||||||
|
// دمياط / ساحل الدلتا
|
||||||
|
const LatLng(31.50, 31.25),
|
||||||
|
// الإسكندرية (أقصى الشمال الغربي)
|
||||||
|
const LatLng(31.20, 29.80),
|
||||||
|
// غرب الدلتا
|
||||||
|
const LatLng(30.50, 30.20),
|
||||||
|
// العودة لنقطة البداية
|
||||||
|
const LatLng(29.20, 30.60),
|
||||||
|
];
|
||||||
|
|
||||||
|
// دالة تُرجع رابط API بناءً على اسم الدولة
|
||||||
|
static String getRoutingApiUrl(String countryName) {
|
||||||
|
switch (countryName) {
|
||||||
|
case 'Jordan':
|
||||||
|
return 'https://routec.intaleq.xyz/route-jo';
|
||||||
|
case 'Syria':
|
||||||
|
return 'https://routec.intaleq.xyz/route';
|
||||||
|
case 'Egypt':
|
||||||
|
return 'https://routec.intaleq.xyz/route-eg';
|
||||||
|
default:
|
||||||
|
// الافتراضي في حالة لم يقع الموقع ضمن أي من المضلعات
|
||||||
|
return 'https://routec.intaleq.xyz/route';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
// import 'package:sefer_driver/env/env.dart';
|
// import 'package:sefer_driver/env/env.dart';
|
||||||
|
|
||||||
import 'package:sefer_driver/env/env.dart';
|
|
||||||
|
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
import 'box_name.dart';
|
import 'box_name.dart';
|
||||||
|
|
||||||
class AppLink {
|
class AppLink {
|
||||||
static String serverPHP = box.read('serverPHP');
|
static String serverPHP = box.read('serverPHP');
|
||||||
|
|
||||||
static String seferPaymentServer =
|
static String paymentServer = 'https://walletintaleq.intaleq.xyz/v1/main';
|
||||||
'https://walletintaleq.intaleq.xyz/v1/main';
|
|
||||||
static String seferPaymentServer0 =
|
|
||||||
'https://walletintaleq.intaleq.xyz/v1/main';
|
|
||||||
|
|
||||||
static final String endPoint = 'https://intaleq.xyz/intaleq';
|
static String locationServer =
|
||||||
|
'https://location.intaleq.xyz/intaleq/ride/location';
|
||||||
|
static const String routeApiBaseUrl =
|
||||||
|
"https://routesjo.intaleq.xyz/route/v1/driving";
|
||||||
|
static final String endPoint = 'https://api.intaleq.xyz/intaleq';
|
||||||
static final String syria = 'https://syria.intaleq.xyz/intaleq';
|
static final String syria = 'https://syria.intaleq.xyz/intaleq';
|
||||||
// 'https://api.tripz-egypt.com/tripz';
|
|
||||||
static final String server = endPoint;
|
static final String server = endPoint;
|
||||||
|
|
||||||
|
///=================ride==========================///
|
||||||
|
///https://api.intaleq.xyz/intaleq/ride
|
||||||
|
static String ride = '$server/ride';
|
||||||
|
static String rideServer = 'https://rides.intaleq.xyz/intaleq/ride';
|
||||||
|
|
||||||
static String seferCairoServer = endPoint;
|
static String seferCairoServer = endPoint;
|
||||||
static String seferGizaServer =
|
static String seferGizaServer =
|
||||||
box.read('Giza') ?? box.read(BoxName.serverChosen);
|
box.read('Giza') ?? box.read(BoxName.serverChosen);
|
||||||
@@ -25,8 +29,7 @@ class AppLink {
|
|||||||
// static final String server = Env.serverPHP;
|
// static final String server = Env.serverPHP;
|
||||||
|
|
||||||
static String loginJwtDriver = "$server/loginJwtDriver.php";
|
static String loginJwtDriver = "$server/loginJwtDriver.php";
|
||||||
static String loginJwtWalletDriver =
|
static String loginJwtWalletDriver = "$server/loginJwtWalletDriver.php";
|
||||||
"$seferPaymentServer/loginJwtWalletDriver.php";
|
|
||||||
static String loginFirstTimeDriver = "$server/loginFirstTimeDriver.php";
|
static String loginFirstTimeDriver = "$server/loginFirstTimeDriver.php";
|
||||||
|
|
||||||
static String googleMapsLink = 'https://maps.googleapis.com/maps/api/';
|
static String googleMapsLink = 'https://maps.googleapis.com/maps/api/';
|
||||||
@@ -35,7 +38,7 @@ class AppLink {
|
|||||||
'https://generativelanguage.googleapis.com/v1beta3/models/text-bison-001:generateText';
|
'https://generativelanguage.googleapis.com/v1beta3/models/text-bison-001:generateText';
|
||||||
|
|
||||||
static String test = "$server/test.php";
|
static String test = "$server/test.php";
|
||||||
static String ride = '$server/ride';
|
|
||||||
//===============contact==========================
|
//===============contact==========================
|
||||||
static String savePhones = "$ride/egyptPhones/add.php";
|
static String savePhones = "$ride/egyptPhones/add.php";
|
||||||
static String savePhonesSyria = "$ride/egyptPhones/syrianAdd.php";
|
static String savePhonesSyria = "$ride/egyptPhones/syrianAdd.php";
|
||||||
@@ -48,25 +51,25 @@ class AppLink {
|
|||||||
static String addTokens = "$ride/firebase/add.php";
|
static String addTokens = "$ride/firebase/add.php";
|
||||||
static String addTokensDriver = "$ride/firebase/addDriver.php";
|
static String addTokensDriver = "$ride/firebase/addDriver.php";
|
||||||
static String addTokensDriverWallet =
|
static String addTokensDriverWallet =
|
||||||
"$seferPaymentServer/ride/firebase/addDriver.php";
|
"$paymentServer/ride/firebase/addDriver.php";
|
||||||
|
|
||||||
//=======================Wallet===================
|
//=======================Wallet===================
|
||||||
static String wallet = '$seferPaymentServer/ride/passengerWallet';
|
static String wallet = '$paymentServer/ride/passengerWallet';
|
||||||
static String walletDriver = '$seferPaymentServer/ride/driverWallet';
|
static String walletDriver = '$paymentServer/ride/driverWallet';
|
||||||
static String getAllPassengerTransaction =
|
static String getAllPassengerTransaction =
|
||||||
"$wallet/getAllPassengerTransaction.php";
|
"$wallet/getAllPassengerTransaction.php";
|
||||||
static String payWithMTNConfirm =
|
static String payWithMTNConfirm =
|
||||||
"$seferPaymentServer/ride/mtn/driver/confirm_payment.php";
|
"$paymentServer/ride/mtn/driver/confirm_payment.php";
|
||||||
static String payWithMTNStart =
|
static String payWithMTNStart =
|
||||||
"$seferPaymentServer/ride/mtn/driver/mtn_start.php";
|
"$paymentServer/ride/mtn/driver/mtn_start.php";
|
||||||
static String payWithSyriatelConfirm =
|
static String payWithSyriatelConfirm =
|
||||||
"$seferPaymentServer/ride/syriatel/driver/confirm_payment.php";
|
"$paymentServer/ride/syriatel/driver/confirm_payment.php";
|
||||||
static String payWithSyriatelStart =
|
static String payWithSyriatelStart =
|
||||||
"$seferPaymentServer/ride/syriatel/driver/start_payment.php";
|
"$paymentServer/ride/syriatel/driver/start_payment.php";
|
||||||
static String payWithEcashDriver =
|
static String payWithEcashDriver =
|
||||||
"$seferPaymentServer/ride/ecash/driver/payWithEcash.php";
|
"$paymentServer/ride/ecash/driver/payWithEcash.php";
|
||||||
static String payWithEcashPassenger =
|
static String payWithEcashPassenger =
|
||||||
"$seferPaymentServer/ride/ecash/passenger/payWithEcash.php";
|
"$paymentServer/ride/ecash/passenger/payWithEcash.php";
|
||||||
// wl.tripz-egypt.com/v1/main/ride/ecash/driver
|
// wl.tripz-egypt.com/v1/main/ride/ecash/driver
|
||||||
static String getWalletByPassenger = "$wallet/getWalletByPassenger.php";
|
static String getWalletByPassenger = "$wallet/getWalletByPassenger.php";
|
||||||
static String getPassengersWallet = "$wallet/get.php";
|
static String getPassengersWallet = "$wallet/get.php";
|
||||||
@@ -77,7 +80,8 @@ class AppLink {
|
|||||||
static String updatePassengersWallet = "$wallet/update.php";
|
static String updatePassengersWallet = "$wallet/update.php";
|
||||||
|
|
||||||
static String getWalletByDriver = "$walletDriver/getWalletByDriver.php";
|
static String getWalletByDriver = "$walletDriver/getWalletByDriver.php";
|
||||||
static String driverStatistic = "$walletDriver/driverStatistic.php";
|
static String driverStatistic =
|
||||||
|
"$endPoint/ride/driverWallet/driverStatistic.php";
|
||||||
static String getDriverDetails =
|
static String getDriverDetails =
|
||||||
"$seferCairoServer/ride/driverWallet/getDriverDetails.php";
|
"$seferCairoServer/ride/driverWallet/getDriverDetails.php";
|
||||||
static String getDriverWeekPaymentMove =
|
static String getDriverWeekPaymentMove =
|
||||||
@@ -98,19 +102,20 @@ class AppLink {
|
|||||||
|
|
||||||
////=======================cancelRide===================
|
////=======================cancelRide===================
|
||||||
|
|
||||||
static String addCancelRideFromPassenger = "$ride/cancelRide/add.php";
|
static String addCancelRideFromPassenger = "$rideServer/cancelRide/add.php";
|
||||||
static String addCancelTripFromDriverAfterApplied =
|
static String addCancelTripFromDriverAfterApplied =
|
||||||
"$ride/cancelRide/addCancelTripFromDriverAfterApplied.php";
|
"$rideServer/cancelRide/addCancelTripFromDriverAfterApplied.php";
|
||||||
static String cancelRide = "$ride/cancelRide/get.php";
|
static String cancelRide = "$rideServer/cancelRide/get.php";
|
||||||
//-----------------ridessss------------------
|
//-----------------ridessss------------------
|
||||||
static String addRides = "$ride/rides/add.php";
|
static String addRides = "$rideServer/rides/add.php";
|
||||||
static String getRides = "$ride/rides/get.php";
|
static String getRides = "$rideServer/rides/get.php";
|
||||||
static String getPlacesSyria = "$ride/places_syria/get.php";
|
static String getPlacesSyria = "$rideServer/places_syria/get.php";
|
||||||
static String getMishwari = "$ride/mishwari/get.php";
|
static String getMishwari = "$rideServer/mishwari/get.php";
|
||||||
static String getMishwariDriver = "$ride/mishwari/getDriver.php";
|
static String getMishwariDriver = "$rideServer/mishwari/getDriver.php";
|
||||||
static String getTripCountByCaptain = "$ride/rides/getTripCountByCaptain.php";
|
static String getTripCountByCaptain =
|
||||||
static String getRideOrderID = "$ride/rides/getRideOrderID.php";
|
"$rideServer/rides/getTripCountByCaptain.php";
|
||||||
static String getRideStatus = "$ride/rides/getRideStatus.php";
|
static String getRideOrderID = "$rideServer/rides/getRideOrderID.php";
|
||||||
|
static String getRideStatus = "$rideServer/rides/getRideStatus.php";
|
||||||
static String getOverLayStatus = "$ride/overLay/get.php";
|
static String getOverLayStatus = "$ride/overLay/get.php";
|
||||||
static String getArgumentAfterAppliedFromBackground =
|
static String getArgumentAfterAppliedFromBackground =
|
||||||
"$ride/overLay/getArgumentAfterAppliedFromBackground.php";
|
"$ride/overLay/getArgumentAfterAppliedFromBackground.php";
|
||||||
@@ -118,14 +123,15 @@ class AppLink {
|
|||||||
static String getapiKey = "$ride/apiKey/get.php";
|
static String getapiKey = "$ride/apiKey/get.php";
|
||||||
|
|
||||||
static String getapiKeySefer = "$ride/apiKey/get.php";
|
static String getapiKeySefer = "$ride/apiKey/get.php";
|
||||||
static String getRideStatusBegin = "$ride/rides/getRideStatusBegin.php";
|
static String getRideStatusBegin = "$rideServer/rides/getRideStatusBegin.php";
|
||||||
static String getRideStatusFromStartApp =
|
static String getRideStatusFromStartApp =
|
||||||
"$ride/rides/getRideStatusFromStartApp.php";
|
"$rideServer/rides/getRideStatusFromStartApp.php";
|
||||||
static String updateRides = "$ride/rides/update.php";
|
static String updateRides = "$rideServer/rides/update.php";
|
||||||
static String updateRideAndCheckIfApplied =
|
static String updateRideAndCheckIfApplied =
|
||||||
"$ride/rides/updateRideAndCheckIfApplied.php";
|
"$rideServer/rides/updateRideAndCheckIfApplied.php";
|
||||||
static String updateStausFromSpeed = "$ride/rides/updateStausFromSpeed.php";
|
static String updateStausFromSpeed =
|
||||||
static String deleteRides = "$ride/rides/delete.php";
|
"$rideServer/rides/updateStausFromSpeed.php";
|
||||||
|
static String deleteRides = "$rideServer/rides/delete.php";
|
||||||
|
|
||||||
//-----------------DriverPayment------------------
|
//-----------------DriverPayment------------------
|
||||||
static String addDriverScam = "$ride/driver_scam/add.php";
|
static String addDriverScam = "$ride/driver_scam/add.php";
|
||||||
@@ -136,35 +142,34 @@ class AppLink {
|
|||||||
static String addKazanPercent = "$ride/kazan/add.php";
|
static String addKazanPercent = "$ride/kazan/add.php";
|
||||||
|
|
||||||
////-----------------DriverPayment------------------
|
////-----------------DriverPayment------------------
|
||||||
static String addDrivePayment = "$seferPaymentServer/ride/payment/add.php";
|
static String addDrivePayment = "$paymentServer/ride/payment/add.php";
|
||||||
static String payWithPayMobCardDriver =
|
static String payWithPayMobCardDriver =
|
||||||
"$seferPaymentServer/ride/payMob/paymob_driver/payWithCard.php";
|
"$paymentServer/ride/payMob/paymob_driver/payWithCard.php";
|
||||||
static String payWithWallet =
|
static String payWithWallet =
|
||||||
"$seferPaymentServer/ride/payMob/paymob_driver/payWithWallet.php";
|
"$paymentServer/ride/payMob/paymob_driver/payWithWallet.php";
|
||||||
static String paymetVerifyDriver =
|
static String paymetVerifyDriver =
|
||||||
"$seferPaymentServer/ride/payMob/paymob_driver/paymet_verfy.php";
|
"$paymentServer/ride/payMob/paymob_driver/paymet_verfy.php";
|
||||||
static String updatePaymetToPaid =
|
static String updatePaymetToPaid =
|
||||||
"$seferPaymentServer/ride/payment/updatePaymetToPaid.php";
|
"$paymentServer/ride/payment/updatePaymetToPaid.php";
|
||||||
static String paymobPayoutDriverWallet =
|
static String paymobPayoutDriverWallet =
|
||||||
"$seferPaymentServer/ride/payMob/paymob_driver/paymob_payout.php'";
|
"$paymentServer/ride/payMob/paymob_driver/paymob_payout.php'";
|
||||||
|
|
||||||
static String addSeferWallet = "$seferPaymentServer/ride/seferWallet/add.php";
|
static String addSeferWallet = "$paymentServer/ride/seferWallet/add.php";
|
||||||
static String getSeferWallet = "$seferPaymentServer/ride/seferWallet/get.php";
|
static String getSeferWallet = "$paymentServer/ride/seferWallet/get.php";
|
||||||
static String addDriverPaymentPoints =
|
static String addDriverPaymentPoints =
|
||||||
"$seferPaymentServer/ride/driverPayment/add.php";
|
"$paymentServer/ride/driverPayment/add.php";
|
||||||
static String addPaymentTokenDriver =
|
static String addPaymentTokenDriver =
|
||||||
"$seferPaymentServer/ride/driverWallet/addPaymentToken.php"; //driverWallet/addPaymentToken.php
|
"$paymentServer/ride/driverWallet/addPaymentToken.php"; //driverWallet/addPaymentToken.php
|
||||||
static String addPaymentTokenPassenger =
|
static String addPaymentTokenPassenger =
|
||||||
"$seferPaymentServer/ride/passengerWallet/addPaymentTokenPassenger.php";
|
"$paymentServer/ride/passengerWallet/addPaymentTokenPassenger.php";
|
||||||
static String getDriverPaymentPoints =
|
static String getDriverPaymentPoints =
|
||||||
"$seferPaymentServer/ride/driverWallet/get.php";
|
"$paymentServer/ride/driverWallet/get.php";
|
||||||
static String getDriverPaymentToday =
|
static String getDriverPaymentToday = "$paymentServer/ride/payment/get.php";
|
||||||
"$seferPaymentServer/ride/payment/get.php";
|
static String getCountRide = "$rideServer/payment/getCountRide.php";
|
||||||
static String getCountRide = "$ride/payment/getCountRide.php";
|
|
||||||
static String getAllPaymentFromRide =
|
static String getAllPaymentFromRide =
|
||||||
"$seferPaymentServer/ride/payment/getAllPayment.php";
|
"$paymentServer/ride/payment/getAllPayment.php";
|
||||||
static String getAllPaymentVisa =
|
static String getAllPaymentVisa =
|
||||||
"$seferPaymentServer/ride/payment/getAllPaymentVisa.php";
|
"$paymentServer/ride/payment/getAllPaymentVisa.php";
|
||||||
|
|
||||||
//-----------------Passenger NotificationCaptain------------------
|
//-----------------Passenger NotificationCaptain------------------
|
||||||
static String addNotificationPassenger =
|
static String addNotificationPassenger =
|
||||||
@@ -268,27 +273,31 @@ class AppLink {
|
|||||||
static String uploadEgypt1 = "$server/uploadEgypt1.php";
|
static String uploadEgypt1 = "$server/uploadEgypt1.php";
|
||||||
|
|
||||||
//==================certifcate==========
|
//==================certifcate==========
|
||||||
static String location = '$endPoint/ride/location';
|
// static String location = '$endPoint/ride/location';
|
||||||
static String getCarsLocationByPassenger = "$location/get.php";
|
|
||||||
static String addpassengerLocation = "$location/addpassengerLocation.php";
|
static String getCarsLocationByPassenger = "$locationServer/get.php";
|
||||||
static String getLocationAreaLinks = "$location/get_location_area_links.php";
|
static String addpassengerLocation =
|
||||||
|
"$locationServer/addpassengerLocation.php";
|
||||||
|
static String getLocationAreaLinks =
|
||||||
|
"$locationServer/get_location_area_links.php";
|
||||||
static String getLatestLocationPassenger =
|
static String getLatestLocationPassenger =
|
||||||
"$location/getLatestLocationPassenger.php";
|
"$locationServer/getLatestLocationPassenger.php";
|
||||||
static String getFemalDriverLocationByPassenger =
|
static String getFemalDriverLocationByPassenger =
|
||||||
"$location/getFemalDriver.php";
|
"$locationServer/getFemalDriver.php";
|
||||||
static String getDriverCarsLocationToPassengerAfterApplied =
|
static String getDriverCarsLocationToPassengerAfterApplied =
|
||||||
"$location/getDriverCarsLocationToPassengerAfterApplied.php";
|
"$locationServer/getDriverCarsLocationToPassengerAfterApplied.php";
|
||||||
static String addCarsLocationByPassenger = "$location/add.php";
|
static String addCarsLocationByPassenger = "$locationServer/add.php";
|
||||||
static String saveBehavior = "$location/save_behavior.php";
|
static String saveBehavior = "$locationServer/save_behavior.php";
|
||||||
static String addCarsLocationGizaEndpoint = "$location/add.php";
|
static String addCarsLocationGizaEndpoint = "$locationServer/add.php";
|
||||||
static String addCarsLocationAlexandriaEndpoint = "$location/add.php";
|
static String addCarsLocationAlexandriaEndpoint = "$locationServer/add.php";
|
||||||
static String addCarsLocationCairoEndpoint = "$location/add.php";
|
static String addCarsLocationCairoEndpoint = "$locationServer/add.php";
|
||||||
static String deleteCarsLocationByPassenger = "$location/delete.php";
|
static String deleteCarsLocationByPassenger = "$locationServer/delete.php";
|
||||||
static String updateCarsLocationByPassenger = "$location/update.php";
|
static String updateCarsLocationByPassenger = "$locationServer/update.php";
|
||||||
static String getTotalDriverDuration = "$location/getTotalDriverDuration.php";
|
static String getTotalDriverDuration =
|
||||||
static String getRidesDriverByDay = "$location/getRidesDriverByDay.php";
|
"$locationServer/getTotalDriverDuration.php";
|
||||||
|
static String getRidesDriverByDay = "$locationServer/getRidesDriverByDay.php";
|
||||||
static String getTotalDriverDurationToday =
|
static String getTotalDriverDurationToday =
|
||||||
"$location/getTotalDriverDurationToday.php";
|
"$locationServer/getTotalDriverDurationToday.php";
|
||||||
|
|
||||||
//==================get_driver_behavior.php=============
|
//==================get_driver_behavior.php=============
|
||||||
static String get_driver_behavior =
|
static String get_driver_behavior =
|
||||||
@@ -333,6 +342,7 @@ class AppLink {
|
|||||||
static String login = "$auth/login.php";
|
static String login = "$auth/login.php";
|
||||||
static String signUp = "$auth/signup.php";
|
static String signUp = "$auth/signup.php";
|
||||||
static String updateDriverClaim = "$auth/captin/updateDriverClaim.php";
|
static String updateDriverClaim = "$auth/captin/updateDriverClaim.php";
|
||||||
|
static String updateShamCashDriver = "$auth/captin/updateShamCashDriver.php";
|
||||||
static String sendVerifyEmail = "$auth/sendVerifyEmail.php";
|
static String sendVerifyEmail = "$auth/sendVerifyEmail.php";
|
||||||
static String passengerRemovedAccountEmail =
|
static String passengerRemovedAccountEmail =
|
||||||
"$auth/passengerRemovedAccountEmail.php";
|
"$auth/passengerRemovedAccountEmail.php";
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
List<String> driverMessages = [
|
List<String> syrianDriverMessages = [
|
||||||
"💸 فرص الربح: افتح تطبيق انطلق الآن وزد دخلك اليوم! المزيد من الطلبات بانتظارك! 🚗",
|
// --- أوقات الذروة والطلبات الكتيرة ---
|
||||||
"🚀 طلبات جديدة: لا تضيع الفرصة! المزيد من الركاب ينتظرونك الآن على تطبيق انطلق. 🏃♂️",
|
"وقت الذروة: البلد مولّعة مشاوير! 🌃 افتح التطبيق بسرعة وبلّش أرباحك تدبّل.",
|
||||||
"📈 زيادة الدخل: حقق أعلى أرباح اليوم مع انطلق! افتح التطبيق وابدأ عملك الآن. 💵",
|
"طلبات كتير حواليك: ⚠️ خليك أول كابتن بيلتقط الطلب وخلّي يومك رابح.",
|
||||||
"🕒 أوقات الذروة: استعد لكسب المزيد خلال فترات الطلب المرتفعة. افتح التطبيق الآن! 📲",
|
"الناس طالعة من الشغل: 🌇 الكل راجع عالبيت، خليك جاهز لرحلات كتيرة!",
|
||||||
"🚗 طلبات جديدة: كن مستعداً، افتح تطبيق انطلق الآن واستقبل المزيد من الطلبات. 🔔",
|
"زحمة المساء: ⏳ الزباين عم تزيد بهالوقت، لا تضيّع الفرصة واشتغل معنا!",
|
||||||
"🎉 فرص النجاح: ابدأ رحلتك إلى النجاح! افتح تطبيق انطلق لزيادة دخلك اليوم. 💼",
|
|
||||||
"🌍 طلبات مرتفعة: المزيد من الركاب بانتظارك، لا تفوّت الفرص، افتح التطبيق الآن! 🚖",
|
// --- التركيز عالربح والمصاري ---
|
||||||
"💪 زيادة الدخل: انطلق نحو تحقيق أهدافك المالية، افتح تطبيق انطلق واكسب المزيد. 🏆",
|
"ضاعف ربحك: 💰 كل مشوار بيقربك من هدفك اليومي. جاهز تنطلق؟",
|
||||||
"💰 أرباح إضافية: افتح التطبيق واستعد لتحقيق أرباح إضافية مع انطلق! المزيد من الطلبات في انتظارك. 🛣️",
|
"مصاري أكتر: جزدانك ناطر تعبك، كل دقيقة شغل هي ربح مضمون. 💪",
|
||||||
"🔥 فرص جديدة: تطبيق انطلق مزدحم الآن! افتح التطبيق وزد أرباحك بفرص جديدة. 🚗",
|
"خليك بالواجهة: الطلب الجاي ممكن يكون إلك، كبسة زر بتجيبلك رزقة اليوم.",
|
||||||
"🚨 طلبات متزايدة: افتح تطبيق انطلق الآن! الطلب مرتفع وفرص الربح كبيرة! 💸",
|
"رزقتك جاهزة: 💸 لا تخلّي الزباين يستنّوا، افتح التطبيق وخليك متأهّب.",
|
||||||
"💼 زيادة الدخل: هل أنت جاهز لتحقيق المزيد من الدخل؟ افتح التطبيق وانطلق الآن! 🚖",
|
|
||||||
"🚗 أوقات الذروة: احجز مقعدك في فترات الطلب العالي، افتح تطبيق انطلق الآن واكسب المزيد. 📈",
|
// --- التقدير والشكر ---
|
||||||
"📲 بدء اليوم: ابدأ يومك مع انطلق، وافتح التطبيق الآن لتزيد من فرص الربح. 💵",
|
"شكراً إلك: 🙏 إنت مو بس سائق، إنت أساس نجاح تطبيق انطلق.",
|
||||||
"💸 فرص مستمرة: لا تفوت فرص الربح، افتح تطبيق انطلق الآن وكن على استعداد للمزيد! 🔔",
|
"نفتخر فيك: 🌟 الكباتن متلك هنن يلي رافعين اسمنا بالعالي.",
|
||||||
"📆 زيادة الطلب: انطلق اليوم واستفد من الطلبات المتزايدة على تطبيق انطلق! افتح التطبيق الآن. 🚗",
|
"أداء ممتاز: 👍 استمر بنفس الروح الحلوة، زباينك مبسوطين منك.",
|
||||||
"💥 دخل إضافي: افتح تطبيق انطلق الآن واستقبل طلبات جديدة تحقق لك المزيد من الدخل. 💰",
|
"نجاح مشترك: 🗺️ كل توصيلة بتعملها بتكبر فيها شركتنا وانت كمان.",
|
||||||
"🏆 فرص مرتفعة: استفد من طلبات اليوم المرتفعة، افتح التطبيق الآن مع انطلق. 📲",
|
|
||||||
"🚗 تفضيل العملاء: كن السائق الذي يختاره الجميع! افتح تطبيق انطلق اليوم واربح المزيد. 🔥",
|
// --- التحفيز والتشجيع ---
|
||||||
"💸 دخل إضافي: فرص الدخل الإضافي في انتظارك! افتح تطبيق انطلق واستمتع بالطلبات المتزايدة. 💼",
|
"صباح النشاط: ☀️ بلّش نهارك بطاقة إيجابية وانطلق لتكسب أكتر.",
|
||||||
|
"كل مشوار فرصة: 🏁 لا توقف، الطريق لإلك والنجاح ناطرك.",
|
||||||
|
"يوم مربح: 💼 السوق ناشط اليوم، لا تفوّت الفرصة!",
|
||||||
|
"جاهز للطلب الجاي: 🔔 الزبون الجاي ممكن يكون أوفر ممتاز، خليك مستعد.",
|
||||||
|
|
||||||
|
// --- نصايح ومعلومات ---
|
||||||
|
"نصيحة اليوم: روح صوب الأسواق والمطاعم، الطلب هناك عالي هالفترة. 🏙️",
|
||||||
|
"حافظ على تقييمك: 😊 الزبون بينجذب للكابتن يلي عنده تقييم عالي وابتسامة.",
|
||||||
|
"جو ممطر: 🌧️ المطر يعني طلبات أكتر، خليك شغّال بهالوقت!",
|
||||||
|
"حدث اليوم: 🎆 في فعالية بالبلد، المشاوير كتيرة بهالمنطقة، استغلها!"
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ Download the Intaleq app now and enjoy your ride!
|
|||||||
// Use the normalized phone number for consistency.
|
// Use the normalized phone number for consistency.
|
||||||
var phone = contact.phones.first.normalizedNumber;
|
var phone = contact.phones.first.normalizedNumber;
|
||||||
if (phone.isNotEmpty) {
|
if (phone.isNotEmpty) {
|
||||||
await CRUD().post(link: AppLink.savePhonesSyria, payload: {
|
CRUD().post(link: AppLink.savePhonesSyria, payload: {
|
||||||
"driverId": box.read(BoxName.driverID), // Associate with driver
|
"driverId": box.read(BoxName.driverID), // Associate with driver
|
||||||
"name": contact.displayName ?? 'No Name',
|
"name": contact.displayName ?? 'No Name',
|
||||||
"phone": phone,
|
"phone": phone,
|
||||||
@@ -112,10 +112,10 @@ Download the Intaleq app now and enjoy your ride!
|
|||||||
|
|
||||||
// 4. After a successful sync, set the flag to prevent future syncs.
|
// 4. After a successful sync, set the flag to prevent future syncs.
|
||||||
await box.write(syncFlagKey, true);
|
await box.write(syncFlagKey, true);
|
||||||
mySnackbarSuccess('Contacts sync completed successfully!'.tr);
|
// mySnackbarSuccess('Contacts sync completed successfully!'.tr);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
mySnackeBarError('An error occurred during contact sync: $e'.tr);
|
// mySnackeBarError('An error occurred during contact sync: $e'.tr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,27 +316,55 @@ Download the Intaleq app now and enjoy your ride!
|
|||||||
/// Formats a phone number to the standard Syrian international format (+963...).
|
/// Formats a phone number to the standard Syrian international format (+963...).
|
||||||
String _formatSyrianPhoneNumber(String input) {
|
String _formatSyrianPhoneNumber(String input) {
|
||||||
String digitsOnly = input.replaceAll(RegExp(r'\D'), '');
|
String digitsOnly = input.replaceAll(RegExp(r'\D'), '');
|
||||||
if (digitsOnly.startsWith('963')) {
|
|
||||||
return '+$digitsOnly';
|
|
||||||
}
|
|
||||||
if (digitsOnly.startsWith('09') && digitsOnly.length == 10) {
|
if (digitsOnly.startsWith('09') && digitsOnly.length == 10) {
|
||||||
return '+963${digitsOnly.substring(1)}';
|
return '963${digitsOnly.substring(1)}';
|
||||||
}
|
}
|
||||||
if (digitsOnly.length == 9 && digitsOnly.startsWith('9')) {
|
if (digitsOnly.length == 9 && digitsOnly.startsWith('9')) {
|
||||||
return '+963$digitsOnly';
|
return '963$digitsOnly';
|
||||||
}
|
}
|
||||||
return input; // Fallback for unrecognized formats
|
return input; // Fallback for unrecognized formats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String normalizeSyrianPhone(String input) {
|
||||||
|
String phone = input.trim();
|
||||||
|
|
||||||
|
// احذف كل شيء غير أرقام
|
||||||
|
phone = phone.replaceAll(RegExp(r'[^0-9]'), '');
|
||||||
|
|
||||||
|
// إذا يبدأ بـ 0 → احذفها
|
||||||
|
if (phone.startsWith('0')) {
|
||||||
|
phone = phone.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// إذا يبدأ بـ 963 مكررة → احذف التكرار
|
||||||
|
while (phone.startsWith('963963')) {
|
||||||
|
phone = phone.substring(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// إذا يبدأ بـ 963 ولكن داخله كمان 963 → خليه مرة واحدة فقط
|
||||||
|
if (phone.startsWith('963') && phone.length > 12) {
|
||||||
|
phone = phone.substring(phone.length - 9); // آخر 9 أرقام
|
||||||
|
}
|
||||||
|
|
||||||
|
// الآن إذا كان بلا 963 → أضفها
|
||||||
|
if (!phone.startsWith('963')) {
|
||||||
|
phone = '963' + phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
|
|
||||||
/// Sends an invitation to a potential new driver.
|
/// Sends an invitation to a potential new driver.
|
||||||
void sendInvite() async {
|
void sendInvite() async {
|
||||||
if (invitePhoneController.text.isEmpty) {
|
if (invitePhoneController.text.isEmpty) {
|
||||||
mySnackeBarError('Please enter a phone number'.tr);
|
mySnackeBarError('Please enter a phone number'.tr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Format Syrian phone number: remove leading 0 and add +963
|
||||||
String formattedPhoneNumber =
|
String formattedPhoneNumber =
|
||||||
_formatSyrianPhoneNumber(invitePhoneController.text);
|
normalizeSyrianPhone(invitePhoneController.text);
|
||||||
if (formattedPhoneNumber.length < 13) {
|
if (formattedPhoneNumber.length != 12) {
|
||||||
mySnackeBarError('Please enter a correct phone'.tr);
|
mySnackeBarError('Please enter a correct phone'.tr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -356,8 +384,8 @@ Download the Intaleq app now and enjoy your ride!
|
|||||||
'${'before'.tr} *${d['message']['expirationTime'].toString()}*\n\n'
|
'${'before'.tr} *${d['message']['expirationTime'].toString()}*\n\n'
|
||||||
'_*${d['message']['inviteCode'].toString()}*_\n\n'
|
'_*${d['message']['inviteCode'].toString()}*_\n\n'
|
||||||
'${"Install our app:".tr}\n'
|
'${"Install our app:".tr}\n'
|
||||||
'*Android:* https://play.google.com/store/apps/details?id=com.sefer_driver\n\n\n'
|
'*Android:* https://play.google.com/store/apps/details?id=com.intaleq_driver \n\n\n'
|
||||||
'*iOS:* https://apps.apple.com/ae/app/sefer-driver/id6502189302';
|
'*iOS:* https://apps.apple.com/st/app/intaleq-driver/id6482995159';
|
||||||
|
|
||||||
launchCommunication('whatsapp', formattedPhoneNumber, message);
|
launchCommunication('whatsapp', formattedPhoneNumber, message);
|
||||||
invitePhoneController.clear();
|
invitePhoneController.clear();
|
||||||
@@ -372,22 +400,32 @@ Download the Intaleq app now and enjoy your ride!
|
|||||||
mySnackeBarError('Please enter a phone number'.tr);
|
mySnackeBarError('Please enter a phone number'.tr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String formattedPhoneNumber =
|
|
||||||
_formatSyrianPhoneNumber(invitePhoneController.text);
|
// Format Syrian phone number: remove leading 0 and add +963
|
||||||
if (formattedPhoneNumber.length < 13) {
|
String formattedPhoneNumber = invitePhoneController.text.trim();
|
||||||
|
if (formattedPhoneNumber.startsWith('0')) {
|
||||||
|
formattedPhoneNumber = formattedPhoneNumber.substring(1);
|
||||||
|
}
|
||||||
|
formattedPhoneNumber = '+963$formattedPhoneNumber';
|
||||||
|
|
||||||
|
if (formattedPhoneNumber.length < 12) {
|
||||||
|
// +963 + 9 digits = 12+
|
||||||
mySnackeBarError('Please enter a correct phone'.tr);
|
mySnackeBarError('Please enter a correct phone'.tr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var response =
|
var response = await CRUD().post(
|
||||||
await CRUD().post(link: AppLink.addInvitationPassenger, payload: {
|
link: AppLink.addInvitationPassenger,
|
||||||
|
payload: {
|
||||||
"driverId": box.read(BoxName.driverID),
|
"driverId": box.read(BoxName.driverID),
|
||||||
"inviterPassengerPhone": formattedPhoneNumber,
|
"inviterPassengerPhone": formattedPhoneNumber,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (response != 'failure') {
|
if (response != 'failure') {
|
||||||
var d = jsonDecode(response);
|
var d = response;
|
||||||
mySnackbarSuccess('Invite sent successfully'.tr);
|
mySnackbarSuccess('Invite sent successfully'.tr);
|
||||||
|
|
||||||
String message = '${'*Intaleq APP CODE*'.tr}\n\n'
|
String message = '${'*Intaleq APP CODE*'.tr}\n\n'
|
||||||
'${"Use this code in registration".tr}\n\n'
|
'${"Use this code in registration".tr}\n\n'
|
||||||
'${"To get a gift for both".tr}\n\n'
|
'${"To get a gift for both".tr}\n\n'
|
||||||
@@ -395,8 +433,8 @@ Download the Intaleq app now and enjoy your ride!
|
|||||||
'${'before'.tr} *${d['message']['expirationTime'].toString()}*\n\n'
|
'${'before'.tr} *${d['message']['expirationTime'].toString()}*\n\n'
|
||||||
'_*${d['message']['inviteCode'].toString()}*_\n\n'
|
'_*${d['message']['inviteCode'].toString()}*_\n\n'
|
||||||
'${"Install our app:".tr}\n'
|
'${"Install our app:".tr}\n'
|
||||||
'*Android:* https://play.google.com/store/apps/details?id=com.mobileapp.store.ride\n\n\n'
|
'*Android:* https://play.google.com/store/apps/details?id=com.Intaleq.intaleq\n\n\n'
|
||||||
'*iOS:* https://apps.apple.com/us/app/sefer/id6458734951';
|
'*iOS:* https://apps.apple.com/st/app/intaleq-rider/id6748075179';
|
||||||
|
|
||||||
launchCommunication('whatsapp', formattedPhoneNumber, message);
|
launchCommunication('whatsapp', formattedPhoneNumber, message);
|
||||||
invitePhoneController.clear();
|
invitePhoneController.clear();
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import '../../../views/auth/captin/otp_page.dart';
|
|||||||
import '../../../views/auth/captin/otp_token_page.dart';
|
import '../../../views/auth/captin/otp_token_page.dart';
|
||||||
import '../../../views/auth/syria/pending_driver_page.dart';
|
import '../../../views/auth/syria/pending_driver_page.dart';
|
||||||
import '../../firebase/firbase_messge.dart';
|
import '../../firebase/firbase_messge.dart';
|
||||||
|
import '../../firebase/local_notification.dart';
|
||||||
|
import '../../firebase/notification_service.dart';
|
||||||
import '../../functions/encrypt_decrypt.dart';
|
import '../../functions/encrypt_decrypt.dart';
|
||||||
import '../../functions/package_info.dart';
|
import '../../functions/package_info.dart';
|
||||||
import '../../functions/secure_storage.dart';
|
import '../../functions/secure_storage.dart';
|
||||||
@@ -67,8 +69,8 @@ class LoginDriverController extends GetxController {
|
|||||||
void onInit() async {
|
void onInit() async {
|
||||||
box.write(BoxName.countryCode, 'Syria');
|
box.write(BoxName.countryCode, 'Syria');
|
||||||
// box.write(BoxName.driverID, '34feffd3fa72d6bee56b');
|
// box.write(BoxName.driverID, '34feffd3fa72d6bee56b');
|
||||||
await getAppTester();
|
// await getAppTester();
|
||||||
|
getJWT();
|
||||||
super.onInit();
|
super.onInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,8 +151,7 @@ class LoginDriverController extends GetxController {
|
|||||||
// Log.print('response.request: ${response1.request}');
|
// Log.print('response.request: ${response1.request}');
|
||||||
// Log.print('response.body: ${response1.body}');
|
// Log.print('response.body: ${response1.body}');
|
||||||
// print(payload);
|
// print(payload);
|
||||||
// Log.print(
|
// Log.print('payment["jwt"]: ${jsonDecode(response1.body)['jwt']}');
|
||||||
// 'jsonDecode(response1.body)["jwt"]: ${jsonDecode(response1.body)['jwt']}');
|
|
||||||
await box.write(BoxName.hmac, jsonDecode(response1.body)['hmac']);
|
await box.write(BoxName.hmac, jsonDecode(response1.body)['hmac']);
|
||||||
return jsonDecode(response1.body)['jwt'].toString();
|
return jsonDecode(response1.body)['jwt'].toString();
|
||||||
}
|
}
|
||||||
@@ -177,11 +178,11 @@ class LoginDriverController extends GetxController {
|
|||||||
Uri.parse(AppLink.loginFirstTimeDriver),
|
Uri.parse(AppLink.loginFirstTimeDriver),
|
||||||
body: payload,
|
body: payload,
|
||||||
);
|
);
|
||||||
// Log.print('response0: ${response0.body}');
|
Log.print('response0: ${response0.body}');
|
||||||
// Log.print('request: ${response0.request}');
|
Log.print('request: ${response0.request}');
|
||||||
if (response0.statusCode == 200) {
|
if (response0.statusCode == 200) {
|
||||||
final decodedResponse1 = jsonDecode(response0.body);
|
final decodedResponse1 = jsonDecode(response0.body);
|
||||||
// Log.print('decodedResponse1: ${decodedResponse1}');
|
Log.print('decodedResponse1: ${decodedResponse1}');
|
||||||
|
|
||||||
final jwt = decodedResponse1['jwt'];
|
final jwt = decodedResponse1['jwt'];
|
||||||
box.write(BoxName.jwt, c(jwt));
|
box.write(BoxName.jwt, c(jwt));
|
||||||
@@ -189,7 +190,7 @@ class LoginDriverController extends GetxController {
|
|||||||
// ✅ بعد التأكد أن كل المفاتيح موجودة
|
// ✅ بعد التأكد أن كل المفاتيح موجودة
|
||||||
await EncryptionHelper.initialize();
|
await EncryptionHelper.initialize();
|
||||||
|
|
||||||
await AppInitializer().getKey();
|
// await AppInitializer().getKey();
|
||||||
} else {}
|
} else {}
|
||||||
} else {
|
} else {
|
||||||
await EncryptionHelper.initialize();
|
await EncryptionHelper.initialize();
|
||||||
@@ -199,7 +200,7 @@ class LoginDriverController extends GetxController {
|
|||||||
'password': box.read(BoxName.emailDriver),
|
'password': box.read(BoxName.emailDriver),
|
||||||
'aud': '${AK.allowed}$dev',
|
'aud': '${AK.allowed}$dev',
|
||||||
};
|
};
|
||||||
print(payload);
|
// print(payload);
|
||||||
var response1 = await http.post(
|
var response1 = await http.post(
|
||||||
Uri.parse(AppLink.loginJwtDriver),
|
Uri.parse(AppLink.loginJwtDriver),
|
||||||
body: payload,
|
body: payload,
|
||||||
@@ -214,7 +215,7 @@ class LoginDriverController extends GetxController {
|
|||||||
final jwt = decodedResponse1['jwt'];
|
final jwt = decodedResponse1['jwt'];
|
||||||
await box.write(BoxName.jwt, c(jwt));
|
await box.write(BoxName.jwt, c(jwt));
|
||||||
|
|
||||||
await AppInitializer().getKey();
|
// await AppInitializer().getKey();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,10 +254,185 @@ class LoginDriverController extends GetxController {
|
|||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isInviteDriverFound = false;
|
||||||
|
|
||||||
|
Future updateInvitationCodeFromRegister() async {
|
||||||
|
var res = await CRUD().post(
|
||||||
|
link: AppLink.updateDriverInvitationDirectly,
|
||||||
|
payload: {
|
||||||
|
"inviterDriverPhone": box.read(BoxName.phoneDriver).toString(),
|
||||||
|
// "driverId": box.read(BoxName.driverID).toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Log.print('invite: ${res}');
|
||||||
|
|
||||||
|
if (res['status'] != 'failure') {
|
||||||
|
isInviteDriverFound = true;
|
||||||
|
update();
|
||||||
|
// mySnackbarSuccess("Code approved".tr); // Localized success message
|
||||||
|
box.write(BoxName.isInstall, '1');
|
||||||
|
NotificationController().showNotification(
|
||||||
|
"Code approved".tr, "Code approved".tr, 'tone2', '');
|
||||||
|
|
||||||
|
NotificationService.sendNotification(
|
||||||
|
target: (res)['message'][0]['token'].toString(),
|
||||||
|
title: 'You have received a gift token!'.tr,
|
||||||
|
body: 'for '.tr + box.read(BoxName.phoneDriver).toString(),
|
||||||
|
isTopic: false, // Important: this is a token
|
||||||
|
tone: 'tone2',
|
||||||
|
driverList: [], category: 'You have received a gift token!',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// mySnackeBarError(
|
||||||
|
// "You dont have invitation code".tr); // Localized error message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loginWithGoogleCredential(String driverID, email) async {
|
loginWithGoogleCredential(String driverID, email) async {
|
||||||
isloading = true;
|
isloading = true;
|
||||||
update();
|
update();
|
||||||
await SecurityHelper.performSecurityChecks();
|
// await SecurityHelper.performSecurityChecks();
|
||||||
|
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
|
||||||
|
// await getJWT();
|
||||||
|
var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: {
|
||||||
|
// 'email': email ?? 'yet',
|
||||||
|
'id': driverID,
|
||||||
|
});
|
||||||
|
Log.print('loginWithGoogleCredential: ${res}');
|
||||||
|
if (res == 'failure') {
|
||||||
|
await isPhoneVerified();
|
||||||
|
isloading = false; // <--- أضفت هذا أيضاً
|
||||||
|
update();
|
||||||
|
return false;
|
||||||
|
// Get.snackbar('Failure', '', backgroundColor: Colors.red);
|
||||||
|
} else {
|
||||||
|
var jsonDecoeded = jsonDecode(res);
|
||||||
|
var d = jsonDecoeded['data'][0];
|
||||||
|
if (jsonDecoeded.isNotEmpty) {
|
||||||
|
if (jsonDecoeded['status'] == 'success' &&
|
||||||
|
d['is_verified'].toString() == '1') {
|
||||||
|
box.write(BoxName.emailDriver, d['email']);
|
||||||
|
box.write(BoxName.firstTimeLoadKey, 'false');
|
||||||
|
box.write(BoxName.driverID, (d['id']));
|
||||||
|
box.write(BoxName.isTest, '1');
|
||||||
|
box.write(BoxName.gender, (d['gender']));
|
||||||
|
box.write(BoxName.phoneVerified, d['is_verified'].toString());
|
||||||
|
box.write(BoxName.phoneDriver, (d['phone']));
|
||||||
|
box.write(BoxName.is_claimed, d['is_claimed']);
|
||||||
|
box.write(BoxName.isInstall, d['isInstall']);
|
||||||
|
// box.write(
|
||||||
|
// BoxName.isGiftToken, d['isGiftToken']);
|
||||||
|
box.write(BoxName.nameArabic, (d['name_arabic']));
|
||||||
|
box.write(BoxName.carYear, d['year']);
|
||||||
|
box.write(BoxName.bankCodeDriver, (d['bankCode']));
|
||||||
|
box.write(BoxName.accountBankNumberDriver, (d['accountBank']));
|
||||||
|
box.write(
|
||||||
|
BoxName.nameDriver,
|
||||||
|
'${(d['first_name'])}'
|
||||||
|
' ${(d['last_name'])}');
|
||||||
|
if (((d['model']).toString().contains('دراجه') ||
|
||||||
|
d['make'].toString().contains('دراجه '))) {
|
||||||
|
if ((d['gender']).toString() == 'Male') {
|
||||||
|
box.write(BoxName.carTypeOfDriver, 'Scooter');
|
||||||
|
} else {
|
||||||
|
box.write(BoxName.carTypeOfDriver, 'Pink Bike');
|
||||||
|
}
|
||||||
|
} else if (int.parse(d['year'].toString()) > 2016) {
|
||||||
|
if (d['gender'].toString() != 'Male') {
|
||||||
|
box.write(BoxName.carTypeOfDriver, 'Lady');
|
||||||
|
} else {
|
||||||
|
box.write(BoxName.carTypeOfDriver, 'Comfort');
|
||||||
|
}
|
||||||
|
} else if (int.parse(d['year'].toString()) > 2002 &&
|
||||||
|
int.parse(d['year'].toString()) < 2016) {
|
||||||
|
box.write(BoxName.carTypeOfDriver, 'Speed');
|
||||||
|
} else if (int.parse(d['year'].toString()) < 2002) {
|
||||||
|
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
|
||||||
|
}
|
||||||
|
|
||||||
|
// add invitations
|
||||||
|
if (box.read(BoxName.isInstall) == null ||
|
||||||
|
box.read(BoxName.isInstall).toString() == '0') {
|
||||||
|
updateInvitationCodeFromRegister();
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateAppTester(AppInformation.appName);
|
||||||
|
if (d['status'].toString() != 'yet') {
|
||||||
|
var token = await CRUD().get(
|
||||||
|
link: AppLink.getDriverToken,
|
||||||
|
payload: {
|
||||||
|
'captain_id': (box.read(BoxName.driverID)).toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||||
|
await storage.write(
|
||||||
|
key: BoxName.fingerPrint, value: fingerPrint.toString());
|
||||||
|
// print(jsonDecode(token)['data'][0]['token'].toString());
|
||||||
|
// print(box.read(BoxName.tokenDriver).toString());
|
||||||
|
// if (box.read(BoxName.emailDriver).toString() !=
|
||||||
|
// '963992952235@intaleqapp.com') {
|
||||||
|
if (token != 'failure') {
|
||||||
|
var serverData = jsonDecode(token);
|
||||||
|
if ((serverData['data'][0]['token'].toString()) !=
|
||||||
|
box.read(BoxName.tokenDriver).toString() ||
|
||||||
|
serverData['data'][0]['fingerPrint'].toString() !=
|
||||||
|
fingerPrint.toString()) {
|
||||||
|
await Get.defaultDialog(
|
||||||
|
barrierDismissible: false,
|
||||||
|
title: 'Device Change Detected'.tr,
|
||||||
|
middleText: 'Please verify your identity'.tr,
|
||||||
|
textConfirm: 'Verify'.tr,
|
||||||
|
confirmTextColor: Colors.white,
|
||||||
|
onConfirm: () {
|
||||||
|
Get.back();
|
||||||
|
// انتقل لصفحة OTP الجديدة
|
||||||
|
Get.to(
|
||||||
|
() => OtpVerificationPage(
|
||||||
|
phone: d['phone'].toString(),
|
||||||
|
deviceToken: fingerPrint.toString(),
|
||||||
|
token: token.toString(),
|
||||||
|
ptoken:
|
||||||
|
jsonDecode(token)['data'][0]['token'].toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
Get.offAll(() => HomeCaptain()); // افترض أن هذا الكلاس موجود
|
||||||
|
isloading = false; // <--- أضفت هذا
|
||||||
|
update(); // <--- أضفت هذا
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Get.offAll(
|
||||||
|
() => DriverVerificationScreen()); // افترض أن هذا الكلاس موجود
|
||||||
|
isloading = false; // <--- أضفت هذا
|
||||||
|
update(); // <--- أضفت هذا
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get.off(() => HomeCaptain());
|
||||||
|
} else {
|
||||||
|
Get.offAll(() => PhoneNumberScreen());
|
||||||
|
isloading = false;
|
||||||
|
update();
|
||||||
|
return false; // <--- ✅ وهذا السطر موجود للحالات الأخرى
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mySnackbarSuccess('');
|
||||||
|
|
||||||
|
isloading = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logintest(String driverID, email) async {
|
||||||
|
isloading = true;
|
||||||
|
update();
|
||||||
|
// await SecurityHelper.performSecurityChecks();
|
||||||
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
|
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
|
||||||
|
|
||||||
var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: {
|
var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: {
|
||||||
@@ -265,15 +441,18 @@ class LoginDriverController extends GetxController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// print('res is $res');
|
// print('res is $res');
|
||||||
if (res == 'failure') {
|
// if (res == 'failure') {
|
||||||
await isPhoneVerified();
|
// await isPhoneVerified();
|
||||||
// Get.snackbar('Failure', '', backgroundColor: Colors.red);
|
// // Get.snackbar('Failure', '', backgroundColor: Colors.red);
|
||||||
} else {
|
// } else
|
||||||
|
// {
|
||||||
var jsonDecoeded = jsonDecode(res);
|
var jsonDecoeded = jsonDecode(res);
|
||||||
var d = jsonDecoeded['data'][0];
|
var d = jsonDecoeded['data'][0];
|
||||||
if (jsonDecoeded.isNotEmpty) {
|
if (jsonDecoeded.isNotEmpty) {
|
||||||
if (jsonDecoeded['status'] == 'success' &&
|
if (jsonDecoeded['status'] == 'success')
|
||||||
d['is_verified'].toString() == '1') {
|
// &&
|
||||||
|
// d['is_verified'].toString() == '1')
|
||||||
|
{
|
||||||
box.write(BoxName.emailDriver, d['email']);
|
box.write(BoxName.emailDriver, d['email']);
|
||||||
box.write(BoxName.firstTimeLoadKey, 'false');
|
box.write(BoxName.firstTimeLoadKey, 'false');
|
||||||
box.write(BoxName.driverID, (d['id']));
|
box.write(BoxName.driverID, (d['id']));
|
||||||
@@ -313,147 +492,28 @@ class LoginDriverController extends GetxController {
|
|||||||
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
|
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
|
||||||
}
|
}
|
||||||
// updateAppTester(AppInformation.appName);
|
// updateAppTester(AppInformation.appName);
|
||||||
if (d['status'].toString() != 'yet') {
|
|
||||||
var token = await CRUD().get(
|
|
||||||
link: AppLink.getDriverToken,
|
|
||||||
payload: {
|
|
||||||
'captain_id': (box.read(BoxName.driverID)).toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
// var token = await CRUD().get(
|
||||||
await storage.write(
|
// link: AppLink.getDriverToken,
|
||||||
key: BoxName.fingerPrint, value: fingerPrint.toString());
|
// payload: {'captain_id': (box.read(BoxName.driverID)).toString()});
|
||||||
// print(jsonDecode(token)['data'][0]['token'].toString());
|
|
||||||
// print(box.read(BoxName.tokenDriver).toString());
|
|
||||||
|
|
||||||
if (token != 'failure') {
|
// String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||||
if ((jsonDecode(token)['data'][0]['token'].toString()) !=
|
// await storage.write(
|
||||||
box.read(BoxName.tokenDriver).toString()) {
|
// key: BoxName.fingerPrint, value: fingerPrint.toString());
|
||||||
await Get.defaultDialog(
|
|
||||||
barrierDismissible: false,
|
|
||||||
title: 'Device Change Detected'.tr,
|
|
||||||
middleText: 'Please verify your identity'.tr,
|
|
||||||
textConfirm: 'Verify'.tr,
|
|
||||||
confirmTextColor: Colors.white,
|
|
||||||
onConfirm: () {
|
|
||||||
// Get.back();
|
|
||||||
// انتقل لصفحة OTP الجديدة
|
|
||||||
Get.to(
|
|
||||||
() => OtpVerificationPage(
|
|
||||||
phone: d['phone'].toString(),
|
|
||||||
deviceToken: fingerPrint.toString(),
|
|
||||||
token: token.toString(),
|
|
||||||
ptoken:
|
|
||||||
jsonDecode(token)['data'][0]['token'].toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Get.offAll(() => HomeCaptain());
|
|
||||||
} else {
|
|
||||||
Get.off(() => DriverVerificationScreen());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get.off(() => HomeCaptain());
|
|
||||||
} else {
|
|
||||||
Get.offAll(() => PhoneNumberScreen());
|
|
||||||
|
|
||||||
isloading = false;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mySnackbarSuccess('');
|
|
||||||
|
|
||||||
isloading = false;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logintest(String driverID, email) async {
|
|
||||||
isloading = true;
|
|
||||||
update();
|
|
||||||
await SecurityHelper.performSecurityChecks();
|
|
||||||
// Log.print('(BoxName.emailDriver): ${box.read(BoxName.emailDriver)}');
|
|
||||||
|
|
||||||
var res = await CRUD().get(link: AppLink.loginFromGoogleCaptin, payload: {
|
|
||||||
'email': email ?? 'yet',
|
|
||||||
'id': driverID,
|
|
||||||
});
|
|
||||||
|
|
||||||
// print('res is $res');
|
|
||||||
if (res == 'failure') {
|
|
||||||
await isPhoneVerified();
|
|
||||||
// Get.snackbar('Failure', '', backgroundColor: Colors.red);
|
|
||||||
} else {
|
|
||||||
var jsonDecoeded = jsonDecode(res);
|
|
||||||
var d = jsonDecoeded['data'][0];
|
|
||||||
if (jsonDecoeded.isNotEmpty) {
|
|
||||||
if (jsonDecoeded['status'] == 'success' &&
|
|
||||||
d['is_verified'].toString() == '1') {
|
|
||||||
box.write(BoxName.emailDriver, d['email']);
|
|
||||||
box.write(BoxName.firstTimeLoadKey, 'false');
|
|
||||||
box.write(BoxName.driverID, (d['id']));
|
|
||||||
box.write(BoxName.isTest, '1');
|
|
||||||
box.write(BoxName.gender, (d['gender']));
|
|
||||||
box.write(BoxName.phoneVerified, d['is_verified'].toString());
|
|
||||||
box.write(BoxName.phoneDriver, (d['phone']));
|
|
||||||
box.write(BoxName.is_claimed, d['is_claimed']);
|
|
||||||
box.write(BoxName.isInstall, d['isInstall']);
|
|
||||||
// box.write(
|
|
||||||
// BoxName.isGiftToken, d['isGiftToken']);
|
|
||||||
box.write(BoxName.nameArabic, (d['name_arabic']));
|
|
||||||
box.write(BoxName.carYear, d['year']);
|
|
||||||
box.write(BoxName.bankCodeDriver, (d['bankCode']));
|
|
||||||
box.write(BoxName.accountBankNumberDriver, (d['accountBank']));
|
|
||||||
box.write(
|
|
||||||
BoxName.nameDriver,
|
|
||||||
'${(d['first_name'])}'
|
|
||||||
' ${(d['last_name'])}');
|
|
||||||
if (((d['model']).toString().contains('دراجه') ||
|
|
||||||
d['make'].toString().contains('دراجه '))) {
|
|
||||||
if ((d['gender']).toString() == 'Male') {
|
|
||||||
box.write(BoxName.carTypeOfDriver, 'Scooter');
|
|
||||||
} else {
|
|
||||||
box.write(BoxName.carTypeOfDriver, 'Pink Bike');
|
|
||||||
}
|
|
||||||
} else if (int.parse(d['year'].toString()) > 2016) {
|
|
||||||
if (d['gender'].toString() != 'Male') {
|
|
||||||
box.write(BoxName.carTypeOfDriver, 'Lady');
|
|
||||||
} else {
|
|
||||||
box.write(BoxName.carTypeOfDriver, 'Comfort');
|
|
||||||
}
|
|
||||||
} else if (int.parse(d['year'].toString()) > 2002 &&
|
|
||||||
int.parse(d['year'].toString()) < 2016) {
|
|
||||||
box.write(BoxName.carTypeOfDriver, 'Speed');
|
|
||||||
} else if (int.parse(d['year'].toString()) < 2002) {
|
|
||||||
box.write(BoxName.carTypeOfDriver, 'Awfar Car');
|
|
||||||
}
|
|
||||||
updateAppTester(AppInformation.appName);
|
|
||||||
|
|
||||||
var token = await CRUD().get(
|
|
||||||
link: AppLink.getDriverToken,
|
|
||||||
payload: {'captain_id': (box.read(BoxName.driverID)).toString()});
|
|
||||||
|
|
||||||
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
|
||||||
await storage.write(
|
|
||||||
key: BoxName.fingerPrint, value: fingerPrint.toString());
|
|
||||||
|
|
||||||
Get.off(() => HomeCaptain());
|
Get.off(() => HomeCaptain());
|
||||||
} else {
|
// } else {
|
||||||
Get.offAll(() => PhoneNumberScreen());
|
// Get.offAll(() => PhoneNumberScreen());
|
||||||
|
|
||||||
isloading = false;
|
// isloading = false;
|
||||||
update();
|
// update();
|
||||||
}
|
// }
|
||||||
} else {
|
// }
|
||||||
mySnackbarSuccess('');
|
// else {
|
||||||
|
// mySnackbarSuccess('');
|
||||||
|
|
||||||
isloading = false;
|
// isloading = false;
|
||||||
update();
|
// update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -530,12 +590,20 @@ class LoginDriverController extends GetxController {
|
|||||||
if (token != 'failure') {
|
if (token != 'failure') {
|
||||||
if ((jsonDecode(token)['data'][0]['token']) !=
|
if ((jsonDecode(token)['data'][0]['token']) !=
|
||||||
(box.read(BoxName.tokenDriver))) {
|
(box.read(BoxName.tokenDriver))) {
|
||||||
Get.put(FirebaseMessagesController()).sendNotificationToDriverMAP(
|
// Get.put(FirebaseMessagesController()).sendNotificationToDriverMAP(
|
||||||
'token change'.tr,
|
// 'token change'.tr,
|
||||||
'change device'.tr,
|
// 'change device'.tr,
|
||||||
(jsonDecode(token)['data'][0]['token']).toString(),
|
// (jsonDecode(token)['data'][0]['token']).toString(),
|
||||||
[],
|
// [],
|
||||||
'ding.wav');
|
// 'ding.wav');
|
||||||
|
NotificationService.sendNotification(
|
||||||
|
target: (jsonDecode(token)['data'][0]['token']).toString(),
|
||||||
|
title: 'token change'.tr,
|
||||||
|
body: 'token change'.tr,
|
||||||
|
isTopic: false, // Important: this is a token
|
||||||
|
tone: 'cancel',
|
||||||
|
driverList: [], category: 'token change',
|
||||||
|
);
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
title: 'you will use this device?'.tr,
|
title: 'you will use this device?'.tr,
|
||||||
middleText: '',
|
middleText: '',
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sefer_driver/print.dart';
|
||||||
import 'package:sefer_driver/views/home/Captin/home_captain/home_captin.dart';
|
import 'package:sefer_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||||
|
|
||||||
import '../../../constant/box_name.dart';
|
import '../../../constant/box_name.dart';
|
||||||
import '../../../constant/links.dart';
|
import '../../../constant/links.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../firebase/firbase_messge.dart';
|
import '../../firebase/firbase_messge.dart';
|
||||||
|
import '../../firebase/notification_service.dart';
|
||||||
import '../../functions/crud.dart';
|
import '../../functions/crud.dart';
|
||||||
|
|
||||||
class OtpVerificationController extends GetxController {
|
class OtpVerificationController extends GetxController {
|
||||||
@@ -87,25 +89,23 @@ class OtpVerificationController extends GetxController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response != 'failure') {
|
if (response != 'failure') {
|
||||||
Get.back(); // توجه إلى الصفحة التالية
|
Log.print('response: ${response}');
|
||||||
|
// Get.back(); // توجه إلى الصفحة التالية
|
||||||
await CRUD().post(
|
await CRUD().post(
|
||||||
link:
|
link: '${AppLink.paymentServer}/auth/token/update_driver_auth.php',
|
||||||
'${AppLink.seferPaymentServer}/auth/token/update_driver_auth.php',
|
|
||||||
payload: {
|
payload: {
|
||||||
'token': box.read(BoxName.tokenDriver).toString(),
|
'token': box.read(BoxName.tokenDriver).toString(),
|
||||||
'fingerPrint': finger.toString(),
|
'fingerPrint': finger.toString(),
|
||||||
'captain_id': box.read(BoxName.driverID).toString(),
|
'captain_id': box.read(BoxName.driverID).toString(),
|
||||||
});
|
});
|
||||||
final fcm = Get.isRegistered<FirebaseMessagesController>()
|
|
||||||
? Get.find<FirebaseMessagesController>()
|
|
||||||
: Get.put(FirebaseMessagesController());
|
|
||||||
|
|
||||||
await fcm.sendNotificationToDriverMAP(
|
await NotificationService.sendNotification(
|
||||||
'token change',
|
target: ptoken.toString(),
|
||||||
'change device'.tr,
|
title: 'token change'.tr,
|
||||||
ptoken.toString(),
|
body: 'token change'.tr,
|
||||||
[],
|
isTopic: false, // Important: this is a token
|
||||||
'cancel.wav',
|
tone: 'cancel',
|
||||||
|
driverList: [], category: 'token change',
|
||||||
);
|
);
|
||||||
|
|
||||||
Get.offAll(() => HomeCaptain());
|
Get.offAll(() => HomeCaptain());
|
||||||
|
|||||||
@@ -18,19 +18,82 @@ class PhoneAuthHelper {
|
|||||||
static final String _sendOtpUrl = '${_baseUrl}sendWhatsAppDriver.php';
|
static final String _sendOtpUrl = '${_baseUrl}sendWhatsAppDriver.php';
|
||||||
static final String _verifyOtpUrl = '${_baseUrl}verifyOtp.php';
|
static final String _verifyOtpUrl = '${_baseUrl}verifyOtp.php';
|
||||||
static final String _registerUrl = '${_baseUrl}register_driver.php';
|
static final String _registerUrl = '${_baseUrl}register_driver.php';
|
||||||
|
static String formatSyrianPhone(String phone) {
|
||||||
|
// Remove spaces, symbols, +, -, ()
|
||||||
|
phone = phone.replaceAll(RegExp(r'[ \-\(\)\+]'), '').trim();
|
||||||
|
|
||||||
|
// Normalize 00963 → 963
|
||||||
|
if (phone.startsWith('00963')) {
|
||||||
|
phone = phone.replaceFirst('00963', '963');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize 0963 → 963
|
||||||
|
if (phone.startsWith('0963')) {
|
||||||
|
phone = phone.replaceFirst('0963', '963');
|
||||||
|
}
|
||||||
|
if (phone.startsWith('096309')) {
|
||||||
|
phone = phone.replaceFirst('096309', '963');
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Fix 96309xxxx → 9639xxxx
|
||||||
|
if (phone.startsWith('96309')) {
|
||||||
|
phone = '9639' + phone.substring(5); // remove the "0" after 963
|
||||||
|
}
|
||||||
|
|
||||||
|
// If starts with 9630 → correct to 9639
|
||||||
|
if (phone.startsWith('9630')) {
|
||||||
|
phone = '9639' + phone.substring(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If already in correct format: 9639xxxxxxxx
|
||||||
|
if (phone.startsWith('9639') && phone.length == 12) {
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If starts with 963 but missing the 9
|
||||||
|
if (phone.startsWith('963') && phone.length > 3) {
|
||||||
|
// Ensure it begins with 9639
|
||||||
|
if (!phone.startsWith('9639')) {
|
||||||
|
phone = '9639' + phone.substring(3);
|
||||||
|
}
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If starts with 09xxxxxxxx → 9639xxxxxxxx
|
||||||
|
if (phone.startsWith('09')) {
|
||||||
|
return '963' + phone.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If 9xxxxxxxx (9 digits)
|
||||||
|
if (phone.startsWith('9') && phone.length == 9) {
|
||||||
|
return '963' + phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If starts with incorrect 0xxxxxxx → assume Syrian and fix
|
||||||
|
if (phone.startsWith('0') && phone.length == 10) {
|
||||||
|
return '963' + phone.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
|
|
||||||
/// Sends an OTP to the provided phone number.
|
/// Sends an OTP to the provided phone number.
|
||||||
static Future<bool> sendOtp(String phoneNumber) async {
|
static Future<bool> sendOtp(String phoneNumber) async {
|
||||||
try {
|
try {
|
||||||
|
final fixedPhone = formatSyrianPhone(phoneNumber);
|
||||||
|
Log.print('fixedPhone: $fixedPhone');
|
||||||
|
|
||||||
final response = await CRUD().post(
|
final response = await CRUD().post(
|
||||||
link: _sendOtpUrl,
|
link: _sendOtpUrl,
|
||||||
payload: {'receiver': phoneNumber},
|
payload: {'receiver': fixedPhone},
|
||||||
);
|
);
|
||||||
|
Log.print('fixedPhone: ${fixedPhone}');
|
||||||
|
|
||||||
if (response != 'failure') {
|
if (response != 'failure') {
|
||||||
final data = (response);
|
final data = (response);
|
||||||
Log.print('data: ${data}');
|
Log.print('data: ${data}');
|
||||||
// if (data['status'] == 'success') {
|
// if (data['status'] == 'success') {
|
||||||
mySnackbarSuccess('An OTP has been sent to your WhatsApp number.'.tr);
|
mySnackbarSuccess('An OTP has been sent to your number.'.tr);
|
||||||
return true;
|
return true;
|
||||||
// } else {
|
// } else {
|
||||||
// mySnackeBarError(data['message'] ?? 'Failed to send OTP.');
|
// mySnackeBarError(data['message'] ?? 'Failed to send OTP.');
|
||||||
@@ -47,11 +110,15 @@ class PhoneAuthHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Verifies the OTP and logs the user in.
|
/// Verifies the OTP and logs the user in.
|
||||||
static Future<void> verifyOtp(String phoneNumber, String otp) async {
|
static Future<void> verifyOtp(String phoneNumber) async {
|
||||||
try {
|
try {
|
||||||
|
final fixedPhone = formatSyrianPhone(phoneNumber);
|
||||||
|
Log.print('fixedPhone: $fixedPhone');
|
||||||
final response = await CRUD().post(
|
final response = await CRUD().post(
|
||||||
link: _verifyOtpUrl,
|
link: _verifyOtpUrl,
|
||||||
payload: {'phone_number': phoneNumber, 'otp': otp},
|
payload: {
|
||||||
|
'phone_number': fixedPhone,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response != 'failure') {
|
if (response != 'failure') {
|
||||||
@@ -59,7 +126,7 @@ class PhoneAuthHelper {
|
|||||||
|
|
||||||
if (data['status'] == 'success') {
|
if (data['status'] == 'success') {
|
||||||
final isRegistered = data['message']['isRegistered'] ?? false;
|
final isRegistered = data['message']['isRegistered'] ?? false;
|
||||||
box.write(BoxName.phoneVerified, true);
|
box.write(BoxName.phoneVerified, '1');
|
||||||
box.write(BoxName.phoneDriver, phoneNumber);
|
box.write(BoxName.phoneDriver, phoneNumber);
|
||||||
box.write(BoxName.driverID, data['message']['driverID']);
|
box.write(BoxName.driverID, data['message']['driverID']);
|
||||||
|
|
||||||
@@ -78,7 +145,7 @@ class PhoneAuthHelper {
|
|||||||
// ✅ رقم الهاتف تم التحقق منه لكن السائق غير مسجل
|
// ✅ رقم الهاتف تم التحقق منه لكن السائق غير مسجل
|
||||||
// mySnackbarSuccess('Phone verified. Please complete registration.');
|
// mySnackbarSuccess('Phone verified. Please complete registration.');
|
||||||
// Get.offAll(() => SyrianCardAI());
|
// Get.offAll(() => SyrianCardAI());
|
||||||
Get.offAll(() => RegistrationView());
|
Get.to(() => RegistrationView());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mySnackeBarError(data['message'] ?? 'Verification failed.');
|
mySnackeBarError(data['message'] ?? 'Verification failed.');
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ class RegisterCaptainController extends GetxController {
|
|||||||
// box.read(BoxName.emailDriver).toString(),
|
// box.read(BoxName.emailDriver).toString(),
|
||||||
// );
|
// );
|
||||||
// Get.offAll(() => SyrianCardAI());
|
// Get.offAll(() => SyrianCardAI());
|
||||||
Get.offAll(() => RegistrationView());
|
Get.to(() => RegistrationView());
|
||||||
// } else {
|
// } else {
|
||||||
// Get.snackbar('title', 'message');
|
// Get.snackbar('title', 'message');
|
||||||
// }
|
// }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:image_cropper/image_cropper.dart';
|
|||||||
import 'package:image/image.dart' as img;
|
import 'package:image/image.dart' as img;
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:sefer_driver/constant/links.dart';
|
import 'package:sefer_driver/constant/links.dart';
|
||||||
|
import 'package:sefer_driver/controller/firebase/notification_service.dart';
|
||||||
import '../../../constant/box_name.dart';
|
import '../../../constant/box_name.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
// --- Final Submission ---
|
// --- Final Submission ---
|
||||||
@@ -57,6 +58,7 @@ class RegistrationController extends GetxController {
|
|||||||
final firstNameController = TextEditingController();
|
final firstNameController = TextEditingController();
|
||||||
final lastNameController = TextEditingController();
|
final lastNameController = TextEditingController();
|
||||||
final nationalIdController = TextEditingController();
|
final nationalIdController = TextEditingController();
|
||||||
|
final bithdateController = TextEditingController();
|
||||||
final phoneController = TextEditingController(); // You can pre-fill this
|
final phoneController = TextEditingController(); // You can pre-fill this
|
||||||
final driverLicenseExpiryController = TextEditingController();
|
final driverLicenseExpiryController = TextEditingController();
|
||||||
DateTime? driverLicenseExpiryDate;
|
DateTime? driverLicenseExpiryDate;
|
||||||
@@ -70,7 +72,26 @@ class RegistrationController extends GetxController {
|
|||||||
final carVinController = TextEditingController(); // Chassis number
|
final carVinController = TextEditingController(); // Chassis number
|
||||||
final carRegistrationExpiryController = TextEditingController();
|
final carRegistrationExpiryController = TextEditingController();
|
||||||
DateTime? carRegistrationExpiryDate;
|
DateTime? carRegistrationExpiryDate;
|
||||||
|
// داخل RegistrationController
|
||||||
|
|
||||||
|
// المتغيرات لتخزين القيم المختارة (لإرسالها للـ API لاحقاً)
|
||||||
|
int? selectedVehicleCategoryId; // سيخزن 1 أو 2 أو 3
|
||||||
|
int? selectedFuelTypeId; // سيخزن 1 أو 2 أو 3 أو 4
|
||||||
|
|
||||||
|
// قائمة أنواع المركبات (مطابقة لقاعدة البيانات)
|
||||||
|
final List<Map<String, dynamic>> vehicleCategoryOptions = [
|
||||||
|
{'id': 1, 'name': 'Car'.tr}, // ترجمة: سيارة
|
||||||
|
{'id': 2, 'name': 'Motorcycle'.tr}, // ترجمة: دراجة نارية
|
||||||
|
{'id': 3, 'name': 'Van / Bus'.tr}, // ترجمة: فان / باص
|
||||||
|
];
|
||||||
|
|
||||||
|
// قائمة أنواع الوقود
|
||||||
|
final List<Map<String, dynamic>> fuelTypeOptions = [
|
||||||
|
{'id': 1, 'name': 'Petrol'.tr}, // ترجمة: بنزين
|
||||||
|
{'id': 2, 'name': 'Diesel'.tr}, // ترجمة: ديزل
|
||||||
|
{'id': 3, 'name': 'Electric'.tr}, // ترجمة: كهربائي
|
||||||
|
{'id': 4, 'name': 'Hybrid'.tr}, // ترجمة: هايبرد
|
||||||
|
];
|
||||||
// STEP 3: Document Uploads
|
// STEP 3: Document Uploads
|
||||||
File? driverLicenseFrontImage;
|
File? driverLicenseFrontImage;
|
||||||
File? driverLicenseBackImage;
|
File? driverLicenseBackImage;
|
||||||
@@ -100,14 +121,15 @@ class RegistrationController extends GetxController {
|
|||||||
isValid = driverInfoFormKey.currentState!.validate();
|
isValid = driverInfoFormKey.currentState!.validate();
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
// Optional: Check if license is expired
|
// Optional: Check if license is expired
|
||||||
if (driverLicenseExpiryDate != null &&
|
// if (driverLicenseExpiryDate != null &&
|
||||||
driverLicenseExpiryDate!.isBefore(DateTime.now())) {
|
// driverLicenseExpiryDate!.isBefore(DateTime.now())) {
|
||||||
Get.snackbar('Expired License', 'Your driver’s license has expired.',
|
// Get.snackbar('Expired License', 'Your driver’s license has expired.'.tr
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
// ,
|
||||||
backgroundColor: Colors.red,
|
// snackPosition: SnackPosition.BOTTOM,
|
||||||
colorText: Colors.white);
|
// backgroundColor: Colors.red,
|
||||||
return; // Stop progression
|
// colorText: Colors.white);
|
||||||
}
|
// return; // Stop progression
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
} else if (currentPage.value == 1) {
|
} else if (currentPage.value == 1) {
|
||||||
// Validate Step 2
|
// Validate Step 2
|
||||||
@@ -461,25 +483,30 @@ class RegistrationController extends GetxController {
|
|||||||
|
|
||||||
Future<void> submitRegistration() async {
|
Future<void> submitRegistration() async {
|
||||||
// 0) دوال/مساعدات محلية
|
// 0) دوال/مساعدات محلية
|
||||||
|
void _addField(Map<String, String> fields, String key, String? value) {
|
||||||
|
if (value != null && value.isNotEmpty) {
|
||||||
|
fields[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1) تحقق من وجود الروابط بدل الملفات
|
// 1) تحقق من وجود الروابط
|
||||||
final driverFrontUrl = docUrls['driver_license_front'];
|
final driverFrontUrl = docUrls['driver_license_front'];
|
||||||
final driverBackUrl = docUrls['driver_license_back'];
|
final driverBackUrl = docUrls['driver_license_back'];
|
||||||
final carFrontUrl = docUrls['car_license_front'];
|
final carFrontUrl = docUrls['car_license_front'];
|
||||||
final carBackUrl = docUrls['car_license_back'];
|
final carBackUrl = docUrls['car_license_back'];
|
||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
update();
|
||||||
|
|
||||||
final registerUri = Uri.parse(AppLink.register_driver_and_car);
|
final registerUri = Uri.parse(AppLink.register_driver_and_car);
|
||||||
|
|
||||||
final client = http.Client();
|
final client = http.Client();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ترويسات مشتركة
|
// ترويسات مشتركة
|
||||||
final bearer =
|
final bearer =
|
||||||
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}';
|
'Bearer ${r(box.read(BoxName.jwt)).split(AppInformation.addd)[0]}';
|
||||||
final hmac = '${box.read(BoxName.hmac)}';
|
final hmac = '${box.read(BoxName.hmac)}';
|
||||||
|
|
||||||
// 2) جهّز طلب التسجيل الرئيسي: حقول فقط + روابط الصور (لا نرفع صور إطلاقًا)
|
|
||||||
final req = http.MultipartRequest('POST', registerUri);
|
final req = http.MultipartRequest('POST', registerUri);
|
||||||
req.headers.addAll({
|
req.headers.addAll({
|
||||||
'Authorization': bearer,
|
'Authorization': bearer,
|
||||||
@@ -494,12 +521,12 @@ class RegistrationController extends GetxController {
|
|||||||
_addField(fields, 'last_name', lastNameController.text);
|
_addField(fields, 'last_name', lastNameController.text);
|
||||||
_addField(fields, 'phone', box.read(BoxName.phoneDriver) ?? '');
|
_addField(fields, 'phone', box.read(BoxName.phoneDriver) ?? '');
|
||||||
_addField(fields, 'national_number', nationalIdController.text);
|
_addField(fields, 'national_number', nationalIdController.text);
|
||||||
|
_addField(fields, 'birthdate', bithdateController.text);
|
||||||
_addField(fields, 'expiry_date', driverLicenseExpiryController.text);
|
_addField(fields, 'expiry_date', driverLicenseExpiryController.text);
|
||||||
_addField(
|
_addField(fields, 'password', 'generated_password_or_token');
|
||||||
fields, 'password', 'generate_your_password_here'); // عدّل حسب منطقك
|
|
||||||
_addField(fields, 'status', 'yet');
|
_addField(fields, 'status', 'yet');
|
||||||
_addField(fields, 'email', 'Not specified');
|
_addField(fields, 'email', 'Not specified');
|
||||||
_addField(fields, 'gender', 'Male');
|
_addField(fields, 'gender', 'Male'); // يفضل ربطها بـ Dropdown أيضاً
|
||||||
|
|
||||||
// --- Car Data ---
|
// --- Car Data ---
|
||||||
_addField(fields, 'vin', 'yet');
|
_addField(fields, 'vin', 'yet');
|
||||||
@@ -507,22 +534,53 @@ class RegistrationController extends GetxController {
|
|||||||
_addField(fields, 'make', carMakeController.text);
|
_addField(fields, 'make', carMakeController.text);
|
||||||
_addField(fields, 'model', carModelController.text);
|
_addField(fields, 'model', carModelController.text);
|
||||||
_addField(fields, 'year', carYearController.text);
|
_addField(fields, 'year', carYearController.text);
|
||||||
_addField(fields, 'expiration_date', driverLicenseExpiryController.text);
|
_addField(
|
||||||
|
fields,
|
||||||
|
'expiration_date',
|
||||||
|
driverLicenseExpiryController
|
||||||
|
.text); // تأكد من أن هذا تاريخ انتهاء السيارة وليس الرخصة
|
||||||
_addField(fields, 'color', carColorController.text);
|
_addField(fields, 'color', carColorController.text);
|
||||||
_addField(fields, 'fuel', 'Gasoline');
|
|
||||||
if (colorHex != null && colorHex!.isNotEmpty) {
|
if (colorHex != null && colorHex!.isNotEmpty) {
|
||||||
_addField(fields, 'color_hex', colorHex!);
|
_addField(fields, 'color_hex', colorHex!);
|
||||||
}
|
}
|
||||||
_addField(fields, 'owner',
|
_addField(fields, 'owner',
|
||||||
'${firstNameController.text} ${lastNameController.text}');
|
'${firstNameController.text} ${lastNameController.text}');
|
||||||
|
|
||||||
// --- روابط الصور المخزنة مسبقًا ---
|
// ============================================================
|
||||||
|
// 🔥 التعديل الجديد: إرسال الأرقام (IDs) لتصنيف المركبة والوقود
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// 1. إرسال رقم تصنيف المركبة (1=سيارة, 2=دراجة...)
|
||||||
|
if (selectedVehicleCategoryId != null) {
|
||||||
|
_addField(fields, 'vehicle_category_id',
|
||||||
|
selectedVehicleCategoryId.toString());
|
||||||
|
} else {
|
||||||
|
_addField(fields, 'vehicle_category_id', '1'); // قيمة افتراضية (سيارة)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. إرسال رقم ونوع الوقود
|
||||||
|
if (selectedFuelTypeId != null) {
|
||||||
|
// إرسال الرقم (للبحث السريع)
|
||||||
|
_addField(fields, 'fuel_type_id', selectedFuelTypeId.toString());
|
||||||
|
|
||||||
|
// إرسال الاسم نصاً (للتوافق مع العمود القديم 'fuel' إذا لزم الأمر)
|
||||||
|
// نبحث عن الاسم داخل القائمة بناءً على الرقم المختار
|
||||||
|
final fuelObj = fuelTypeOptions.firstWhere(
|
||||||
|
(e) => e['id'] == selectedFuelTypeId,
|
||||||
|
orElse: () => {'name': 'Petrol'});
|
||||||
|
_addField(fields, 'fuel', fuelObj['name'].toString());
|
||||||
|
} else {
|
||||||
|
_addField(fields, 'fuel_type_id', '1');
|
||||||
|
_addField(fields, 'fuel', 'Petrol');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- روابط الصور ---
|
||||||
_addField(fields, 'driver_license_front', driverFrontUrl!);
|
_addField(fields, 'driver_license_front', driverFrontUrl!);
|
||||||
_addField(fields, 'driver_license_back', driverBackUrl!);
|
_addField(fields, 'driver_license_back', driverBackUrl!);
|
||||||
_addField(fields, 'car_license_front', carFrontUrl!);
|
_addField(fields, 'car_license_front', carFrontUrl!);
|
||||||
_addField(fields, 'car_license_back', carBackUrl!);
|
_addField(fields, 'car_license_back', carBackUrl!);
|
||||||
|
|
||||||
// أضف الحقول
|
|
||||||
req.fields.addAll(fields);
|
req.fields.addAll(fields);
|
||||||
|
|
||||||
// 3) الإرسال
|
// 3) الإرسال
|
||||||
@@ -530,73 +588,53 @@ class RegistrationController extends GetxController {
|
|||||||
await client.send(req).timeout(const Duration(seconds: 60));
|
await client.send(req).timeout(const Duration(seconds: 60));
|
||||||
final resp = await http.Response.fromStream(streamed);
|
final resp = await http.Response.fromStream(streamed);
|
||||||
|
|
||||||
// 4) فحص النتيجة
|
// 4) معالجة الاستجابة
|
||||||
Map<String, dynamic>? json;
|
Map<String, dynamic>? json;
|
||||||
try {
|
try {
|
||||||
json = jsonDecode(resp.body) as Map<String, dynamic>;
|
json = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
if (resp.statusCode == 200 && json?['status'] == 'success') {
|
if (resp.statusCode == 200 && json?['status'] == 'success') {
|
||||||
// final driverID =
|
Get.snackbar('Success'.tr, 'Registration completed successfully!'.tr,
|
||||||
// (json!['data']?['driverID'] ?? json['driverID'])?.toString();
|
backgroundColor: Colors.green, colorText: Colors.white);
|
||||||
// if (driverID != null && driverID.isNotEmpty) {
|
|
||||||
// box.write(BoxName.driverID, driverID);
|
|
||||||
// }
|
|
||||||
|
|
||||||
Get.snackbar(
|
// منطق التوكن والإشعارات وتسجيل الدخول...
|
||||||
'Success'.tr,
|
|
||||||
'Registration completed successfully!'.tr,
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
|
|
||||||
// متابعة تسجيل الدخول إن لزم
|
|
||||||
final email = box.read(BoxName.emailDriver);
|
final email = box.read(BoxName.emailDriver);
|
||||||
final driverID = box.read(BoxName.driverID);
|
final driverID = box.read(BoxName.driverID);
|
||||||
final c = Get.isRegistered<LoginDriverController>()
|
|
||||||
? Get.find<LoginDriverController>()
|
|
||||||
: Get.put(LoginDriverController());
|
|
||||||
//token to server
|
|
||||||
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
String fingerPrint = await DeviceHelper.getDeviceFingerprint();
|
||||||
await CRUD().post(link: AppLink.addTokensDriver, payload: {
|
await CRUD().post(link: AppLink.addTokensDriver, payload: {
|
||||||
'captain_id': (box.read(BoxName.driverID)).toString(),
|
'captain_id': (box.read(BoxName.driverID)).toString(),
|
||||||
'token': (box.read(BoxName.tokenDriver)).toString(),
|
'token': (box.read(BoxName.tokenDriver)).toString(),
|
||||||
'fingerPrint': fingerPrint.toString(),
|
'fingerPrint': fingerPrint.toString(),
|
||||||
});
|
});
|
||||||
await CRUD().post(link: AppLink.addTokensDriverWallet, payload: {
|
|
||||||
'token': box.read(BoxName.tokenDriver).toString(),
|
|
||||||
'fingerPrint': fingerPrint.toString(),
|
|
||||||
'captain_id': box.read(BoxName.driverID).toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
NotificationService.sendNotification(
|
||||||
|
target: 'service',
|
||||||
|
title: 'New Driver Registration',
|
||||||
|
body: 'Driver $driverID has submitted registration.',
|
||||||
|
isTopic: true,
|
||||||
|
category: 'new_service_request',
|
||||||
|
);
|
||||||
|
|
||||||
|
final c = Get.isRegistered<LoginDriverController>()
|
||||||
|
? Get.find<LoginDriverController>()
|
||||||
|
: Get.put(LoginDriverController());
|
||||||
c.loginWithGoogleCredential(driverID, email);
|
c.loginWithGoogleCredential(driverID, email);
|
||||||
} else {
|
} else {
|
||||||
final msg =
|
final msg = (json?['message'] ?? 'Registration failed.').toString();
|
||||||
(json?['message'] ?? 'Registration failed. Please try again.')
|
Get.snackbar('Error'.tr, msg,
|
||||||
.toString();
|
backgroundColor: Colors.red, colorText: Colors.white);
|
||||||
Log.print('msg: $msg');
|
|
||||||
Get.snackbar(
|
|
||||||
'Error'.tr,
|
|
||||||
msg,
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Get.snackbar(
|
Get.snackbar('Error'.tr, 'Error: $e',
|
||||||
'Error'.tr,
|
backgroundColor: Colors.red, colorText: Colors.white);
|
||||||
'${'An unexpected error occurred:'.tr} $e',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} // Future<void> submitRegistration() async {
|
|
||||||
// // 1) تحقق من الصور
|
// // 1) تحقق من الصور
|
||||||
// if (driverLicenseFrontImage == null ||
|
// if (driverLicenseFrontImage == null ||
|
||||||
// driverLicenseBackImage == null ||
|
// driverLicenseBackImage == null ||
|
||||||
|
|||||||
@@ -1,30 +1,26 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:sefer_driver/constant/api_key.dart';
|
|
||||||
import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart';
|
import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart';
|
||||||
|
import 'package:sefer_driver/views/home/Captin/orderCaptin/order_speed_request.dart';
|
||||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||||
|
|
||||||
import '../../constant/box_name.dart';
|
import '../../constant/box_name.dart';
|
||||||
import '../../constant/colors.dart';
|
import '../../constant/colors.dart';
|
||||||
import '../../constant/style.dart';
|
import '../../constant/style.dart';
|
||||||
import '../../env/env.dart';
|
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
import '../../print.dart';
|
import '../../print.dart';
|
||||||
import '../../views/auth/captin/criminal_documents_page.dart';
|
import '../../views/auth/captin/criminal_documents_page.dart';
|
||||||
import '../../views/home/Captin/home_captain/home_captin.dart';
|
import '../../views/home/Captin/home_captain/home_captin.dart';
|
||||||
import '../../views/home/Captin/orderCaptin/order_speed_request.dart';
|
|
||||||
import '../../views/home/Captin/orderCaptin/order_request_page.dart';
|
import '../../views/home/Captin/orderCaptin/order_request_page.dart';
|
||||||
import '../../views/home/Captin/orderCaptin/vip_order_page.dart';
|
import '../../views/home/Captin/orderCaptin/vip_order_page.dart';
|
||||||
import '../auth/google_sign.dart';
|
import '../auth/google_sign.dart';
|
||||||
import '../functions/encrypt_decrypt.dart';
|
|
||||||
import '../functions/face_detect.dart';
|
import '../functions/face_detect.dart';
|
||||||
import 'access_token.dart';
|
import '../home/captin/map_driver_controller.dart';
|
||||||
import 'local_notification.dart';
|
import 'local_notification.dart';
|
||||||
|
|
||||||
class FirebaseMessagesController extends GetxController {
|
class FirebaseMessagesController extends GetxController {
|
||||||
@@ -72,9 +68,12 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
|
|
||||||
Future getToken() async {
|
Future getToken() async {
|
||||||
fcmToken.getToken().then((token) {
|
fcmToken.getToken().then((token) {
|
||||||
Log.print('token: ${token}');
|
Log.print('token fcm driver: ${token}');
|
||||||
box.write(BoxName.tokenDriver, (token!));
|
box.write(BoxName.tokenDriver, (token!));
|
||||||
});
|
});
|
||||||
|
// 🔹 الاشتراك في topic
|
||||||
|
await fcmToken.subscribeToTopic("drivers"); // أو "users" حسب نوع المستخدم
|
||||||
|
print("Subscribed to 'drivers' topic ✅");
|
||||||
|
|
||||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
||||||
// If the app is in the background or terminated, show a system tray message
|
// If the app is in the background or terminated, show a system tray message
|
||||||
@@ -82,122 +81,117 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
AndroidNotification? android = notification?.android;
|
AndroidNotification? android = notification?.android;
|
||||||
// if (notification != null && android != null) {
|
// if (notification != null && android != null) {
|
||||||
|
|
||||||
if (message.data.isNotEmpty && message.notification != null) {
|
if (message.data.isNotEmpty) {
|
||||||
fireBaseTitles(message);
|
|
||||||
}
|
|
||||||
if (message.data.isNotEmpty && message.notification != null) {
|
|
||||||
fireBaseTitles(message);
|
fireBaseTitles(message);
|
||||||
}
|
}
|
||||||
|
// if (message.data.isNotEmpty && message.notification != null) {
|
||||||
|
// fireBaseTitles(message);
|
||||||
|
// }
|
||||||
});
|
});
|
||||||
FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {});
|
FirebaseMessaging.onBackgroundMessage((RemoteMessage message) async {});
|
||||||
|
|
||||||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
||||||
if (message.data.isNotEmpty && message.notification != null) {
|
if (message.data.isNotEmpty) {
|
||||||
fireBaseTitles(message);
|
fireBaseTitles(message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fireBaseTitles(RemoteMessage message) async {
|
Future<void> fireBaseTitles(RemoteMessage message) async {
|
||||||
if (message.notification!.title! == 'Order') {
|
// [!! تعديل جوهري !!]
|
||||||
if (Platform.isAndroid) {
|
// اقرأ "النوع" من حمولة البيانات، وليس من العنوان
|
||||||
notificationController.showNotification(
|
String category = message.data['category'] ?? '';
|
||||||
message.notification!.title.toString(),
|
|
||||||
message.notification!.body.toString(),
|
|
||||||
'tone1',
|
|
||||||
'');
|
|
||||||
}
|
|
||||||
// await FirebaseMessagesController().showOverlayNotification(message);
|
|
||||||
var myListString = message.data['DriverList'];
|
|
||||||
// var points = message.data['PolylineJson'];
|
|
||||||
|
|
||||||
|
// اقرأ العنوان والنص (للعرض)
|
||||||
|
String title = message.notification?.title ?? '';
|
||||||
|
String body = message.notification?.body ?? '';
|
||||||
|
|
||||||
|
// استخدم switch لسهولة القراءة والصيانة
|
||||||
|
switch (category) {
|
||||||
|
case 'ORDER':
|
||||||
|
case 'Order': // Handle both cases for backward compatibility
|
||||||
|
// if (Platform.isAndroid) {
|
||||||
|
// notificationController.showNotification(title, body, 'order', '');
|
||||||
|
// }
|
||||||
|
var myListString = message.data['DriverList'];
|
||||||
|
if (myListString != null) {
|
||||||
var myList = jsonDecode(myListString) as List<dynamic>;
|
var myList = jsonDecode(myListString) as List<dynamic>;
|
||||||
// var myPoints = jsonDecode(points) as List<dynamic>;
|
|
||||||
driverToken = myList[14].toString();
|
driverToken = myList[14].toString();
|
||||||
// This is for location using and uploading status
|
|
||||||
Get.put(HomeCaptainController()).changeRideId();
|
Get.put(HomeCaptainController()).changeRideId();
|
||||||
update();
|
update();
|
||||||
Get.to(() => OrderRequestPage(), arguments: {
|
Get.toNamed('/OrderRequestPage', arguments: {
|
||||||
// Get.to(() => OrderRequestPage(), arguments: {
|
|
||||||
'myListString': myListString,
|
'myListString': myListString,
|
||||||
'DriverList': myList,
|
'DriverList': myList,
|
||||||
// 'PolylineJson': myPoints,
|
'body': body
|
||||||
'body': message.notification!.body
|
|
||||||
});
|
});
|
||||||
} else if (message.notification!.title == 'OrderVIP') {
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'OrderVIP':
|
||||||
var myListString = message.data['DriverList'];
|
var myListString = message.data['DriverList'];
|
||||||
|
if (myListString != null) {
|
||||||
var myList = jsonDecode(myListString) as List<dynamic>;
|
var myList = jsonDecode(myListString) as List<dynamic>;
|
||||||
|
|
||||||
// driverToken = myList[10].toString();
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
notificationController.showNotification(
|
notificationController.showNotification(title, body, 'order', '');
|
||||||
'OrderVIP'.tr, 'OrderVIP'.tr, 'order', '');
|
|
||||||
}
|
}
|
||||||
Get.to(VipOrderPage(), arguments: {
|
Get.to(VipOrderPage(), arguments: {
|
||||||
'myListString': myListString,
|
'myListString': myListString,
|
||||||
'DriverList': myList,
|
'DriverList': myList,
|
||||||
// 'PolylineJson': myPoints,
|
'body': body
|
||||||
'body': message.notification!.body
|
|
||||||
});
|
});
|
||||||
} else if (message.notification!.title == 'Cancel Trip'.tr) {
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
notificationController.showNotification(
|
|
||||||
'Cancel Trip'.tr, 'Passenger Cancel Trip'.tr, 'cancel', '');
|
|
||||||
}
|
}
|
||||||
cancelTripDialog();
|
break;
|
||||||
} else if (message.notification!.title == 'VIP Order') {
|
|
||||||
var myListString = message.data['DriverList'];
|
|
||||||
var driverList = jsonDecode(myListString) as List<dynamic>;
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
notificationController.showNotification(
|
|
||||||
'VIP Order'.tr, '', 'order', '');
|
|
||||||
}
|
|
||||||
MyDialog().getDialog('VIP Order'.tr, 'midTitle', () {
|
|
||||||
sendNotificationToPassengerToken(
|
|
||||||
'VIP Order Accepted'.tr,
|
|
||||||
'The driver accepted your trip'.tr,
|
|
||||||
driverList[0],
|
|
||||||
[driverList[1]],
|
|
||||||
'order');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get.to(const VipOrderPage());
|
case 'Cancel Trip':
|
||||||
} else if (message.notification!.title == 'message From passenger') {
|
case 'TRIP_CANCELLED':
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
notificationController.showNotification(
|
notificationController.showNotification(
|
||||||
'message From passenger'.tr, ''.tr, 'ding', '');
|
title, 'Passenger Cancel Trip'.tr, 'cancel', '');
|
||||||
}
|
}
|
||||||
MyDialog().getDialog(
|
Log.print("🔔 FCM: Ride Cancelled by Passenger received.");
|
||||||
'message From passenger'.tr, message.notification!.body!, () {
|
|
||||||
|
// 1. استخراج السبب (أرسلناه من PHP باسم 'reason')
|
||||||
|
String reason = message.data['reason'] ?? 'No reason provided';
|
||||||
|
|
||||||
|
// 2. توجيه الأمر للكنترولر
|
||||||
|
if (Get.isRegistered<MapDriverController>()) {
|
||||||
|
// استدعاء الحارس (سيتجاهل الأمر إذا كان السوكيت قد سبقه)
|
||||||
|
Get.find<MapDriverController>()
|
||||||
|
.processRideCancelledByPassenger(reason, source: "FCM");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'VIP Order Accepted':
|
||||||
|
// This seems to be a notification for the passenger, but if the driver needs to see it:
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
notificationController.showNotification(title, body, 'order', '');
|
||||||
|
}
|
||||||
|
// Maybe show a simple snackbar confirmation
|
||||||
|
mySnackbarSuccess('You accepted the VIP order.'.tr);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'message From passenger':
|
||||||
|
case 'MSG_FROM_PASSENGER':
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
notificationController.showNotification(title, body, 'ding', '');
|
||||||
|
}
|
||||||
|
MyDialog().getDialog(title, body, () {
|
||||||
Get.back();
|
Get.back();
|
||||||
});
|
});
|
||||||
} else if (message.notification!.title == 'Cancel') {
|
break;
|
||||||
if (Platform.isAndroid) {
|
|
||||||
notificationController.showNotification(
|
case 'token change':
|
||||||
'Cancel'.tr, ''.tr, 'cancel', '');
|
case 'TOKEN_CHANGE':
|
||||||
}
|
|
||||||
MyDialog().getDialog(
|
|
||||||
'Passenger Cancel Trip'.tr,
|
|
||||||
'Trip Cancelled. The cost of the trip will be added to your wallet.'
|
|
||||||
.tr, () {
|
|
||||||
box.write(BoxName.rideStatus, 'Cancel');
|
|
||||||
Log.print('rideStatus from 184 : ${box.read(BoxName.rideStatus)}');
|
|
||||||
Get.offAll(HomeCaptain());
|
|
||||||
});
|
|
||||||
// cancelTripDialog1();
|
|
||||||
} else if (message.notification!.title! == 'token change') {
|
|
||||||
// notificationController
|
|
||||||
// .showNotification('token change'.tr, 'token change', 'cancel');
|
|
||||||
// GoogleSignInHelper.signOut();
|
|
||||||
GoogleSignInHelper.signOut();
|
GoogleSignInHelper.signOut();
|
||||||
} else if (message.notification!.title! == 'face detect') {
|
break;
|
||||||
|
|
||||||
|
case 'face detect':
|
||||||
|
case 'FACE_DETECT':
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
notificationController.showNotification(
|
notificationController.showNotification(title, body, 'tone2', '');
|
||||||
'face detect'.tr, ''.tr, 'tone2', '');
|
|
||||||
}
|
}
|
||||||
String result0 = await faceDetector();
|
String result0 = await faceDetector();
|
||||||
// Handle the result here, e.g., show a dialog or update the UI
|
|
||||||
var result = jsonDecode(result0);
|
var result = jsonDecode(result0);
|
||||||
MyDialogContent().getDialog(
|
MyDialogContent().getDialog(
|
||||||
'Face Detection Result'.tr,
|
'Face Detection Result'.tr,
|
||||||
@@ -211,105 +205,39 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
Get.back();
|
Get.back();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
update();
|
||||||
|
break;
|
||||||
|
|
||||||
update();
|
case 'Hi ,I will go now':
|
||||||
} else if (message.notification!.title! == 'Hi ,I will go now') {
|
case 'PASSENGER_COMING':
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
notificationController.showNotification(
|
notificationController.showNotification(title, body, 'tone2', '');
|
||||||
'Passenger come to you'.tr, 'Hi ,I will go now'.tr, 'tone2', '');
|
|
||||||
}
|
}
|
||||||
update();
|
update();
|
||||||
} else if (message.notification!.title! == 'Call Income'.tr) {
|
break;
|
||||||
try {
|
|
||||||
var myListString = message.data['passengerList'];
|
case 'Criminal Document Required':
|
||||||
var driverList = jsonDecode(myListString) as List<dynamic>;
|
case 'DOC_REQUIRED':
|
||||||
// if (Platform.isAndroid) {
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
notificationController.showNotification('Call Income'.tr,
|
notificationController.showNotification(title, body, 'tone2', '');
|
||||||
message.notification!.body!, 'iphone_ringtone', '');
|
|
||||||
}
|
}
|
||||||
// }
|
MyDialog().getDialog(title, 'You should have upload it .'.tr, () {
|
||||||
// Assuming GetMaterialApp is initialized and context is valid for navigation
|
|
||||||
// Get.to(() => PassengerCallPage(
|
|
||||||
// channelName: driverList[1].toString(),
|
|
||||||
// token: driverList[0].toString(),
|
|
||||||
// remoteID: driverList[2].toString(),
|
|
||||||
// ));
|
|
||||||
} catch (e) {}
|
|
||||||
} else if (message.notification!.title! ==
|
|
||||||
'Call Income from Passenger'.tr) {
|
|
||||||
try {
|
|
||||||
var myListString = message.data['passengerList'];
|
|
||||||
var driverList = jsonDecode(myListString) as List<dynamic>;
|
|
||||||
// if (Platform.isAndroid) {
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
notificationController.showNotification('Call Income'.tr,
|
|
||||||
message.notification!.body!, 'iphone_ringtone', '');
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
// Assuming GetMaterialApp is initialized and context is valid for navigation
|
|
||||||
// Get.to(() => CallPage(
|
|
||||||
// // channelName: driverList[1].toString(),
|
|
||||||
// // token: driverList[0].toString(),
|
|
||||||
// // remoteID: driverList[2].toString(),
|
|
||||||
// ));
|
|
||||||
} catch (e) {}
|
|
||||||
} else if (message.notification!.title! ==
|
|
||||||
"Criminal Document Required".tr) {
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
notificationController.showNotification("Criminal Document Required".tr,
|
|
||||||
message.notification!.body!, 'tone2', '');
|
|
||||||
}
|
|
||||||
MyDialog().getDialog(
|
|
||||||
"Criminal Document Required".tr, 'You should have upload it .'.tr,
|
|
||||||
() {
|
|
||||||
Get.to(() => const CriminalDocumemtPage());
|
Get.to(() => const CriminalDocumemtPage());
|
||||||
});
|
});
|
||||||
Get.to(() => const CriminalDocumemtPage());
|
break;
|
||||||
} else if (message.notification!.title! == 'Call End'.tr) {
|
|
||||||
try {
|
case 'Order Applied':
|
||||||
var myListString = message.data['passengerList'];
|
case 'ORDER_TAKEN':
|
||||||
var driverList = jsonDecode(myListString) as List<dynamic>;
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
notificationController.showNotification(
|
|
||||||
'Call End'.tr, message.notification!.body!, 'tone2', '');
|
|
||||||
}
|
|
||||||
// Assuming GetMaterialApp is initialized and context is valid for navigation
|
|
||||||
// Get.off(const CallPage());
|
|
||||||
} catch (e) {}
|
|
||||||
} else if (message.notification!.title! == 'Order Applied'.tr) {
|
|
||||||
mySnackbarSuccess("The order has been accepted by another driver.".tr);
|
mySnackbarSuccess("The order has been accepted by another driver.".tr);
|
||||||
} else if (message.notification!.title! == 'Order') {
|
break;
|
||||||
if (Platform.isAndroid) {
|
|
||||||
notificationController.showNotification(
|
|
||||||
message.notification!.title.toString(),
|
|
||||||
message.notification!.body.toString(),
|
|
||||||
'order',
|
|
||||||
'');
|
|
||||||
}
|
|
||||||
var myListString = message.data['DriverList'];
|
|
||||||
// var points = message.data['PolylineJson'];
|
|
||||||
|
|
||||||
var myList = jsonDecode(myListString) as List<dynamic>;
|
default:
|
||||||
// var myPoints = jsonDecode(points) as List<dynamic>;
|
Log.print('Received unhandled notification category: $category');
|
||||||
driverToken = myList[14].toString();
|
// Optionally show a generic notification
|
||||||
Get.put(HomeCaptainController()).changeRideId();
|
|
||||||
update();
|
|
||||||
Get.to(() => OrderSpeedRequest(), arguments: {
|
|
||||||
'myListString': myListString,
|
|
||||||
'DriverList': myList,
|
|
||||||
// 'PolylineJson': myPoints,
|
|
||||||
'body': message.notification!.body
|
|
||||||
});
|
|
||||||
} else if (message.notification!.title! == 'Order Applied'.tr) {
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
notificationController.showNotification(
|
notificationController.showNotification(title, body, 'default', '');
|
||||||
'The order Accepted by another Driver'.tr,
|
|
||||||
'We regret to inform you that another driver has accepted this order.'
|
|
||||||
.tr,
|
|
||||||
'order',
|
|
||||||
'');
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,383 +295,6 @@ class FirebaseMessagesController extends GetxController {
|
|||||||
Get.offAll(HomeCaptain());
|
Get.offAll(HomeCaptain());
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future<dynamic> driverArrivePassengerDialoge() {
|
|
||||||
// return Get.defaultDialog(
|
|
||||||
// barrierDismissible: false,
|
|
||||||
// title: 'Hi ,I Arrive your site'.tr,
|
|
||||||
// middleText: 'Please go to Car Driver'.tr,
|
|
||||||
// confirm: MyElevatedButton(
|
|
||||||
// title: 'Ok I will go now.'.tr,
|
|
||||||
// onPressed: () {
|
|
||||||
// FirebaseMessagesController().sendNotificationToPassengerToken(
|
|
||||||
// 'Hi ,I will go now'.tr,
|
|
||||||
// 'I will go now'.tr,
|
|
||||||
// Get.find<MapDriverController>().driverToken, []);
|
|
||||||
// Get.find<MapPassengerController>()
|
|
||||||
// .startTimerDriverWaitPassenger5Minute();
|
|
||||||
|
|
||||||
// Get.back();
|
|
||||||
// }));
|
|
||||||
// }
|
|
||||||
|
|
||||||
Future<dynamic> passengerDialog(String message) {
|
|
||||||
return Get.defaultDialog(
|
|
||||||
barrierDismissible: false,
|
|
||||||
title: 'message From passenger'.tr,
|
|
||||||
titleStyle: AppStyle.title,
|
|
||||||
middleTextStyle: AppStyle.title,
|
|
||||||
middleText: message.tr,
|
|
||||||
confirm: MyElevatedButton(
|
|
||||||
title: 'Ok'.tr,
|
|
||||||
onPressed: () {
|
|
||||||
// FirebaseMessagesController().sendNotificationToPassengerToken(
|
|
||||||
// 'Hi ,I will go now'.tr,
|
|
||||||
// 'I will go now'.tr,
|
|
||||||
// Get.find<MapPassengerController>().driverToken, []);
|
|
||||||
// Get.find<MapPassengerController>()
|
|
||||||
// .startTimerDriverWaitPassenger5Minute();
|
|
||||||
|
|
||||||
Get.back();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
late String serviceAccountKeyJson;
|
|
||||||
@override
|
|
||||||
Future<void> onInit() async {
|
|
||||||
super.onInit();
|
|
||||||
try {
|
|
||||||
var encryptedKey = Env.privateKeyFCM;
|
|
||||||
// Log.print('encryptedKey: ${encryptedKey}');
|
|
||||||
serviceAccountKeyJson =
|
|
||||||
EncryptionHelper.instance.decryptData(encryptedKey);
|
|
||||||
// Log.print('serviceAccountKeyJson: ${serviceAccountKeyJson}');
|
|
||||||
} catch (e) {
|
|
||||||
print('🔴 Error decrypting FCM key: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendNotificationAll(String title, body, tone) async {
|
|
||||||
// Get the token you want to subtract.
|
|
||||||
String token = box.read(BoxName.tokenFCM);
|
|
||||||
tokens = box.read(BoxName.tokens);
|
|
||||||
// Subtract the token from the list of tokens.
|
|
||||||
tokens.remove(token);
|
|
||||||
|
|
||||||
// Save the list of tokens back to the box.
|
|
||||||
// box.write(BoxName.tokens, tokens);
|
|
||||||
tokens = box.read(BoxName.tokens);
|
|
||||||
for (var i = 0; i < tokens.length; i++) {
|
|
||||||
if (serviceAccountKeyJson.isEmpty) {
|
|
||||||
print("🔴 Error: Service Account Key is empty");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Initialize AccessTokenManager
|
|
||||||
final accessTokenManager = AccessTokenManager(serviceAccountKeyJson);
|
|
||||||
|
|
||||||
// Obtain an OAuth 2.0 access token
|
|
||||||
final accessToken = await accessTokenManager.getAccessToken();
|
|
||||||
// Log.print('accessToken: ${accessToken}');
|
|
||||||
|
|
||||||
// Send the notification
|
|
||||||
final response = await http
|
|
||||||
.post(
|
|
||||||
Uri.parse(
|
|
||||||
'https://fcm.googleapis.com/v1/projects/ride-b1bd8/messages:send'),
|
|
||||||
headers: <String, String>{
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer $accessToken',
|
|
||||||
},
|
|
||||||
body: jsonEncode({
|
|
||||||
'message': {
|
|
||||||
'token': token,
|
|
||||||
'notification': {
|
|
||||||
'title': title,
|
|
||||||
'body': body,
|
|
||||||
},
|
|
||||||
// 'data': {
|
|
||||||
// 'DriverList': jsonEncode(data),
|
|
||||||
// },
|
|
||||||
'android': {
|
|
||||||
'priority': 'high', // Set priority to high
|
|
||||||
'notification': {
|
|
||||||
'sound': tone,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'apns': {
|
|
||||||
'headers': {
|
|
||||||
'apns-priority': '10', // Set APNs priority to 10
|
|
||||||
},
|
|
||||||
'payload': {
|
|
||||||
'aps': {
|
|
||||||
'sound': tone,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.whenComplete(() {})
|
|
||||||
.catchError((e) {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendNotificationToPassengerToken(
|
|
||||||
String title, body, token, List<String> map, String tone,
|
|
||||||
{int retryCount = 2}) async {
|
|
||||||
try {
|
|
||||||
if (serviceAccountKeyJson.isEmpty) {
|
|
||||||
print("🔴 Error: Service Account Key is empty");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Initialize AccessTokenManager
|
|
||||||
final accessTokenManager = AccessTokenManager(serviceAccountKeyJson);
|
|
||||||
|
|
||||||
// Obtain an OAuth 2.0 access token
|
|
||||||
final accessToken = await accessTokenManager.getAccessToken();
|
|
||||||
// Log.print('accessToken: ${accessToken}');
|
|
||||||
|
|
||||||
// Send the notification
|
|
||||||
final response = await http.post(
|
|
||||||
Uri.parse(
|
|
||||||
'https://fcm.googleapis.com/v1/projects/ride-b1bd8/messages:send'),
|
|
||||||
headers: <String, String>{
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer $accessToken',
|
|
||||||
},
|
|
||||||
body: jsonEncode({
|
|
||||||
'message': {
|
|
||||||
'token': token,
|
|
||||||
'notification': {
|
|
||||||
'title': title,
|
|
||||||
'body': body,
|
|
||||||
},
|
|
||||||
'data': {
|
|
||||||
'passengerList': jsonEncode(map),
|
|
||||||
},
|
|
||||||
'android': {
|
|
||||||
'priority': 'high', // Set priority to high
|
|
||||||
'notification': {
|
|
||||||
'sound': tone,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'apns': {
|
|
||||||
'headers': {
|
|
||||||
'apns-priority': '10', // Set APNs priority to 10
|
|
||||||
},
|
|
||||||
'payload': {
|
|
||||||
'aps': {
|
|
||||||
'sound': tone,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
print(
|
|
||||||
'Notification sent successfully. Status code: ${response.statusCode}');
|
|
||||||
print('Response body: ${response.body}');
|
|
||||||
} else {
|
|
||||||
print(
|
|
||||||
'Failed to send notification. Status code: ${response.statusCode}');
|
|
||||||
|
|
||||||
print('Response body: ${response.body}');
|
|
||||||
if (retryCount > 0) {
|
|
||||||
print('Retrying... Attempts remaining: $retryCount');
|
|
||||||
await Future.delayed(
|
|
||||||
const Duration(seconds: 2)); // Optional delay before retrying
|
|
||||||
return sendNotificationToPassengerToken(title, body, token, map, tone,
|
|
||||||
retryCount: retryCount - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error sending notification: $e');
|
|
||||||
if (retryCount > 0) {
|
|
||||||
print('Retrying... Attempts remaining: $retryCount');
|
|
||||||
await Future.delayed(
|
|
||||||
const Duration(seconds: 2)); // Optional delay before retrying
|
|
||||||
return sendNotificationToPassengerToken(title, body, token, map, tone,
|
|
||||||
retryCount: retryCount - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendNotificationToPassengerTokenCALL(
|
|
||||||
String title, body, token, List<String> map, String tone,
|
|
||||||
{int retryCount = 2}) async {
|
|
||||||
try {
|
|
||||||
if (serviceAccountKeyJson.isEmpty) {
|
|
||||||
print("🔴 Error: Service Account Key is empty");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Initialize AccessTokenManager
|
|
||||||
final accessTokenManager = AccessTokenManager(serviceAccountKeyJson);
|
|
||||||
|
|
||||||
// Obtain an OAuth 2.0 access token
|
|
||||||
final accessToken = await accessTokenManager.getAccessToken();
|
|
||||||
// Log.print('accessToken: ${accessToken}');
|
|
||||||
|
|
||||||
// Send the notification
|
|
||||||
final response = await http.post(
|
|
||||||
Uri.parse(
|
|
||||||
'https://fcm.googleapis.com/v1/projects/ride-b1bd8/messages:send'),
|
|
||||||
headers: <String, String>{
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer $accessToken',
|
|
||||||
},
|
|
||||||
body: jsonEncode({
|
|
||||||
'message': {
|
|
||||||
'token': token,
|
|
||||||
'notification': {
|
|
||||||
'title': title,
|
|
||||||
'body': body,
|
|
||||||
},
|
|
||||||
'data': {
|
|
||||||
'passengerList': jsonEncode(map),
|
|
||||||
},
|
|
||||||
'android': {
|
|
||||||
'priority': 'high', // Set priority to high
|
|
||||||
'notification': {
|
|
||||||
'sound': tone,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'apns': {
|
|
||||||
'headers': {
|
|
||||||
'apns-priority': '10', // Set APNs priority to 10
|
|
||||||
},
|
|
||||||
'payload': {
|
|
||||||
'aps': {
|
|
||||||
'sound': tone,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
print(
|
|
||||||
'Notification sent successfully. Status code: ${response.statusCode}');
|
|
||||||
print('Response body: ${response.body}');
|
|
||||||
} else {
|
|
||||||
print(
|
|
||||||
'Failed to send notification. Status code: ${response.statusCode}');
|
|
||||||
|
|
||||||
print('Response body: ${response.body}');
|
|
||||||
if (retryCount > 0) {
|
|
||||||
print('Retrying... Attempts remaining: $retryCount');
|
|
||||||
await Future.delayed(
|
|
||||||
const Duration(seconds: 2)); // Optional delay before retrying
|
|
||||||
return sendNotificationToPassengerTokenCALL(
|
|
||||||
title, body, token, map, tone,
|
|
||||||
retryCount: retryCount - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error sending notification: $e');
|
|
||||||
if (retryCount > 0) {
|
|
||||||
print('Retrying... Attempts remaining: $retryCount');
|
|
||||||
await Future.delayed(
|
|
||||||
const Duration(seconds: 2)); // Optional delay before retrying
|
|
||||||
return sendNotificationToPassengerTokenCALL(
|
|
||||||
title, body, token, map, tone,
|
|
||||||
retryCount: retryCount - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> sendNotificationToDriverMAP(
|
|
||||||
String title, String body, String token, List<String> data, String tone,
|
|
||||||
{int retryCount = 2}) async {
|
|
||||||
try {
|
|
||||||
if (serviceAccountKeyJson.isEmpty) {
|
|
||||||
print("🔴 Error: Service Account Key is empty");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize AccessTokenManager
|
|
||||||
final accessTokenManager = AccessTokenManager(serviceAccountKeyJson);
|
|
||||||
Log.print(
|
|
||||||
'accessTokenManager: ${accessTokenManager.serviceAccountJsonKey}');
|
|
||||||
|
|
||||||
// Obtain an OAuth 2.0 access token
|
|
||||||
final accessToken = await accessTokenManager.getAccessToken();
|
|
||||||
// Log.print('accessToken: ${accessToken}');
|
|
||||||
|
|
||||||
// Send the notification
|
|
||||||
final response = await http.post(
|
|
||||||
Uri.parse(
|
|
||||||
'https://fcm.googleapis.com/v1/projects/intaleq-d48a7/messages:send'),
|
|
||||||
headers: <String, String>{
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer $accessToken',
|
|
||||||
},
|
|
||||||
body: jsonEncode({
|
|
||||||
'message': {
|
|
||||||
'token': token,
|
|
||||||
'notification': {
|
|
||||||
'title': title,
|
|
||||||
'body': body,
|
|
||||||
},
|
|
||||||
'data': {
|
|
||||||
'DriverList': jsonEncode(data),
|
|
||||||
},
|
|
||||||
'android': {
|
|
||||||
'priority': 'high', // Set priority to high
|
|
||||||
'notification': {
|
|
||||||
'sound': tone,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'apns': {
|
|
||||||
'headers': {
|
|
||||||
'apns-priority': '10', // Set APNs priority to 10
|
|
||||||
},
|
|
||||||
'payload': {
|
|
||||||
'aps': {
|
|
||||||
'sound': tone,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
print(
|
|
||||||
'Notification sent successfully. Status code: ${response.statusCode}');
|
|
||||||
// print('Response token: ${token}');
|
|
||||||
} else {
|
|
||||||
print(
|
|
||||||
'Failed to send notification. Status code: ${response.statusCode}');
|
|
||||||
print('Response body: ${response.body}');
|
|
||||||
if (retryCount > 0) {
|
|
||||||
print('Retrying... Attempts remaining: $retryCount');
|
|
||||||
await Future.delayed(
|
|
||||||
Duration(seconds: 2)); // Optional delay before retrying
|
|
||||||
return sendNotificationToDriverMAP(title, body, token, data, tone,
|
|
||||||
retryCount: retryCount - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
print('Error sending notification: $e');
|
|
||||||
if (retryCount > 0) {
|
|
||||||
print('Retrying... Attempts remaining: $retryCount');
|
|
||||||
await Future.delayed(
|
|
||||||
Duration(seconds: 2)); // Optional delay before retrying
|
|
||||||
return sendNotificationToDriverMAP(title, body, token, data, tone,
|
|
||||||
retryCount: retryCount - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> removeInvalidToken(String token) async {
|
|
||||||
// Remove token from your database/storage
|
|
||||||
// This prevents future attempts to send to invalid tokens
|
|
||||||
print('Removing invalid token from database: $token');
|
|
||||||
// Your database cleanup logic here
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class OverlayContent extends StatelessWidget {
|
class OverlayContent extends StatelessWidget {
|
||||||
|
|||||||
@@ -1,65 +1,410 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:ui'; // للألوان
|
||||||
|
|
||||||
import 'package:sefer_driver/constant/colors.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:sefer_driver/views/home/Captin/orderCaptin/order_request_page.dart';
|
|
||||||
import 'package:sefer_driver/views/home/Captin/orderCaptin/order_speed_request.dart';
|
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
import 'package:timezone/data/latest.dart' as tz;
|
import 'package:timezone/data/latest.dart' as tz;
|
||||||
import 'package:timezone/timezone.dart' as tz;
|
import 'package:timezone/timezone.dart' as tz;
|
||||||
|
|
||||||
import '../../constant/box_name.dart';
|
import '../../constant/box_name.dart';
|
||||||
import '../../main.dart';
|
import '../../constant/links.dart';
|
||||||
|
import '../../main.dart'; // للوصول لـ box
|
||||||
import '../../print.dart';
|
import '../../print.dart';
|
||||||
import '../../views/notification/notification_captain.dart';
|
import '../../views/home/Captin/driver_map_page.dart';
|
||||||
|
import '../../views/home/Captin/orderCaptin/order_request_page.dart';
|
||||||
|
import '../functions/crud.dart';
|
||||||
import '../home/captin/home_captain_controller.dart';
|
import '../home/captin/home_captain_controller.dart';
|
||||||
|
|
||||||
class NotificationController extends GetxController {
|
class NotificationController extends GetxController {
|
||||||
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
|
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
|
||||||
FlutterLocalNotificationsPlugin();
|
FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// 1. تهيئة الإشعارات (إعداد القنوات والأزرار للآيفون والأندرويد)
|
||||||
|
// ==============================================================================
|
||||||
Future<void> initNotifications() async {
|
Future<void> initNotifications() async {
|
||||||
|
// إعدادات الأندرويد
|
||||||
const AndroidInitializationSettings android =
|
const AndroidInitializationSettings android =
|
||||||
AndroidInitializationSettings('@mipmap/launcher_icon');
|
AndroidInitializationSettings('@mipmap/launcher_icon');
|
||||||
DarwinInitializationSettings ios = DarwinInitializationSettings(
|
|
||||||
|
// إعدادات أزرار الآيفون (Categories)
|
||||||
|
// هذا الجزء ضروري لظهور الأزرار في iOS
|
||||||
|
final List<DarwinNotificationCategory> darwinNotificationCategories = [
|
||||||
|
DarwinNotificationCategory(
|
||||||
|
'ORDER_CATEGORY', // المعرف المستخدم لربط الإشعار بالأزرار
|
||||||
|
actions: [
|
||||||
|
DarwinNotificationAction.plain('ACCEPT_ORDER', '✅ قبول'),
|
||||||
|
DarwinNotificationAction.plain('SHOW_DETAILS', '📄 تفاصيل'),
|
||||||
|
DarwinNotificationAction.plain(
|
||||||
|
'REJECT_ORDER',
|
||||||
|
'❌ رفض',
|
||||||
|
options: {
|
||||||
|
DarwinNotificationActionOption.destructive
|
||||||
|
}, // يظهر باللون الأحمر
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
// إعدادات الآيفون العامة
|
||||||
|
final DarwinInitializationSettings ios = DarwinInitializationSettings(
|
||||||
requestAlertPermission: true,
|
requestAlertPermission: true,
|
||||||
requestBadgePermission: true,
|
requestBadgePermission: true,
|
||||||
requestSoundPermission: true,
|
requestSoundPermission: true,
|
||||||
// onDidReceiveLocalNotification:
|
notificationCategories: darwinNotificationCategories, // تسجيل الأزرار
|
||||||
// (int id, String? title, String? body, String? payload) async {},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
InitializationSettings initializationSettings =
|
InitializationSettings initializationSettings =
|
||||||
InitializationSettings(android: android, iOS: ios);
|
InitializationSettings(android: android, iOS: ios);
|
||||||
|
|
||||||
tz.initializeTimeZones();
|
tz.initializeTimeZones();
|
||||||
print('Notifications initialized');
|
print('✅ Notifications initialized with Action Buttons Support');
|
||||||
|
|
||||||
await _flutterLocalNotificationsPlugin.initialize(
|
await _flutterLocalNotificationsPlugin.initialize(
|
||||||
initializationSettings,
|
initializationSettings,
|
||||||
onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
|
onDidReceiveNotificationResponse: onDidReceiveNotificationResponse,
|
||||||
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
|
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a notification channel
|
// إنشاء قناة الأندرويد ذات الأهمية القصوى
|
||||||
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||||
'high_importance_channel', // Use the same ID as in strings.xml
|
'high_importance_channel',
|
||||||
'High Importance Notifications',
|
'High Importance Notifications',
|
||||||
description: 'This channel is used for important notifications.',
|
description: 'This channel is used for important notifications.',
|
||||||
importance: Importance.high,
|
importance: Importance.max, // أقصى أهمية
|
||||||
|
playSound: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Register the channel with the system
|
|
||||||
await _flutterLocalNotificationsPlugin
|
await _flutterLocalNotificationsPlugin
|
||||||
.resolvePlatformSpecificImplementation<
|
.resolvePlatformSpecificImplementation<
|
||||||
AndroidFlutterLocalNotificationsPlugin>()
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
?.createNotificationChannel(channel);
|
?.createNotificationChannel(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Displays a notification with the given title and message
|
// ==============================================================================
|
||||||
|
// 2. دالة عرض الإشعار المطور (شكل واضح + أزرار + صوت مخصص)
|
||||||
|
// ==============================================================================
|
||||||
|
void showOrderNotification(
|
||||||
|
String title, String body, String tone, String myListString) async {
|
||||||
|
// أ) تنسيق النص والبيانات بشكل جميل
|
||||||
|
String formattedBigText = body;
|
||||||
|
String summaryText = 'طلب جديد';
|
||||||
|
String price = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
List<dynamic> data = jsonDecode(myListString);
|
||||||
|
// استخراج البيانات (تأكد أن الاندكسات مطابقة للباك إند عندك)
|
||||||
|
price = _getVal(data, 26);
|
||||||
|
String distance = _getVal(data, 5);
|
||||||
|
String startLoc = _getVal(data, 29);
|
||||||
|
String endLoc = _getVal(data, 30);
|
||||||
|
String paxName = _getVal(data, 8);
|
||||||
|
// String rating = _getVal(data, 33);
|
||||||
|
|
||||||
|
// تنسيق النص ليكون 4 أسطر واضحة
|
||||||
|
formattedBigText = "👤 $paxName\n"
|
||||||
|
"💰 $price ${'SYP'.tr} | 🛣️ $distance كم\n"
|
||||||
|
"🟢 من: $startLoc\n"
|
||||||
|
"🏁 إلى: $endLoc";
|
||||||
|
|
||||||
|
summaryText = 'سعر الرحلة: $price';
|
||||||
|
} catch (e) {
|
||||||
|
print("Error formatting notification text: $e");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ب) نمط النص الكبير (BigText) للأندرويد
|
||||||
|
BigTextStyleInformation bigTextStyleInformation = BigTextStyleInformation(
|
||||||
|
formattedBigText,
|
||||||
|
contentTitle: '🚖 $title',
|
||||||
|
summaryText: summaryText,
|
||||||
|
htmlFormatContent: true,
|
||||||
|
htmlFormatContentTitle: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ج) معالجة اسم الصوت (أندرويد بدون امتداد، آيفون مع امتداد)
|
||||||
|
String soundNameAndroid = tone.contains('.') ? tone.split('.').first : tone;
|
||||||
|
String soundNameIOS = tone.contains('.') ? tone : "$tone.wav";
|
||||||
|
|
||||||
|
// د) إعدادات الأندرويد (الأزرار + Full Screen)
|
||||||
|
final androidDetails = AndroidNotificationDetails(
|
||||||
|
'high_importance_channel',
|
||||||
|
'High Importance Notifications',
|
||||||
|
importance: Importance.max,
|
||||||
|
priority: Priority.max,
|
||||||
|
fullScreenIntent: true, // يفتح الشاشة وتظهر التفاصيل
|
||||||
|
category: AndroidNotificationCategory.call, // يعامل كمكالمة (رنين مستمر)
|
||||||
|
visibility: NotificationVisibility.public,
|
||||||
|
ongoing: true, // يمنع الحذف بالسحب
|
||||||
|
sound: RawResourceAndroidNotificationSound(soundNameAndroid),
|
||||||
|
audioAttributesUsage: AudioAttributesUsage.alarm, // صوت عالٍ كالمنبه
|
||||||
|
styleInformation: bigTextStyleInformation,
|
||||||
|
color: const Color(0xFF1A252F),
|
||||||
|
|
||||||
|
// الأزرار الثلاثة
|
||||||
|
actions: <AndroidNotificationAction>[
|
||||||
|
const AndroidNotificationAction(
|
||||||
|
'ACCEPT_ORDER',
|
||||||
|
'✅ قبول فوري',
|
||||||
|
showsUserInterface: true,
|
||||||
|
titleColor: Color(0xFF4CAF50), // أخضر
|
||||||
|
),
|
||||||
|
const AndroidNotificationAction(
|
||||||
|
'SHOW_DETAILS',
|
||||||
|
'📄 التفاصيل',
|
||||||
|
showsUserInterface: true,
|
||||||
|
titleColor: Color(0xFF2196F3), // أزرق
|
||||||
|
),
|
||||||
|
const AndroidNotificationAction(
|
||||||
|
'REJECT_ORDER',
|
||||||
|
'❌ رفض',
|
||||||
|
showsUserInterface: false, // لا يفتح التطبيق
|
||||||
|
cancelNotification: true,
|
||||||
|
titleColor: Color(0xFFE53935), // أحمر
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// هـ) إعدادات الآيفون
|
||||||
|
final iosDetails = DarwinNotificationDetails(
|
||||||
|
sound: soundNameIOS,
|
||||||
|
presentAlert: true,
|
||||||
|
presentBadge: true,
|
||||||
|
presentSound: true,
|
||||||
|
categoryIdentifier: 'ORDER_CATEGORY', // ربط الأزرار
|
||||||
|
interruptionLevel: InterruptionLevel.critical, // محاولة لكسر الصامت
|
||||||
|
);
|
||||||
|
|
||||||
|
final details =
|
||||||
|
NotificationDetails(android: androidDetails, iOS: iosDetails);
|
||||||
|
|
||||||
|
// عرض الإشعار
|
||||||
|
await _flutterLocalNotificationsPlugin.show(
|
||||||
|
1001, // ID ثابت لاستبدال الإشعار القديم
|
||||||
|
title,
|
||||||
|
"$price - مسافة $formattedBigText", // نص مختصر يظهر في البار العلوي
|
||||||
|
details,
|
||||||
|
payload: jsonEncode({
|
||||||
|
'type': 'Order',
|
||||||
|
'data': myListString,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// 3. معالجة الاستجابة (عند الضغط على الأزرار)
|
||||||
|
// ==============================================================================
|
||||||
|
Future<void> handleNotificationResponse(NotificationResponse response) async {
|
||||||
|
final payload = response.payload;
|
||||||
|
if (payload == null) return;
|
||||||
|
|
||||||
|
final payloadData = jsonDecode(payload) as Map<String, dynamic>;
|
||||||
|
final rawData = payloadData['data'];
|
||||||
|
|
||||||
|
List<dynamic> listData = [];
|
||||||
|
if (rawData is String) {
|
||||||
|
listData = jsonDecode(rawData);
|
||||||
|
} else if (rawData is List) {
|
||||||
|
listData = rawData;
|
||||||
|
}
|
||||||
|
|
||||||
|
print("🔔 Notification Action: ${response.actionId}");
|
||||||
|
|
||||||
|
// أ) زر القبول
|
||||||
|
if (response.actionId == 'ACCEPT_ORDER') {
|
||||||
|
await _flutterLocalNotificationsPlugin.cancel(1001); // حذف الإشعار
|
||||||
|
_processAcceptOrder(listData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ب) زر التفاصيل
|
||||||
|
else if (response.actionId == 'SHOW_DETAILS') {
|
||||||
|
// await _flutterLocalNotificationsPlugin.cancel(1001); // اختياري: حذف الإشعار
|
||||||
|
Get.to(() => OrderRequestPage(), arguments: {'myListString': rawData});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ج) زر الرفض
|
||||||
|
else if (response.actionId == 'REJECT_ORDER') {
|
||||||
|
await _flutterLocalNotificationsPlugin.cancel(1001); // حذف الإشعار
|
||||||
|
_processRejectOrder(listData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// د) الضغط على الإشعار نفسه (بدون أزرار)
|
||||||
|
else {
|
||||||
|
Get.to(() => OrderRequestPage(), arguments: {'myListString': rawData});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// 4. منطق القبول الآمن (Safe Accept Logic)
|
||||||
|
// ==============================================================================
|
||||||
|
Future<void> _processAcceptOrder(List<dynamic> data) async {
|
||||||
|
// إظهار Loading
|
||||||
|
Get.dialog(
|
||||||
|
WillPopScope(
|
||||||
|
onWillPop: () async => false,
|
||||||
|
child: const Center(
|
||||||
|
child: CircularProgressIndicator(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final driverId = box.read(BoxName.driverID);
|
||||||
|
String orderId = _getVal(data, 16);
|
||||||
|
String passengerToken = _getVal(data, 9);
|
||||||
|
|
||||||
|
print("🚀 Sending Accept Request for Order: $orderId");
|
||||||
|
|
||||||
|
var res = await CRUD().post(
|
||||||
|
link: "${AppLink.ride}/rides/acceptRide.php",
|
||||||
|
payload: {
|
||||||
|
'id': orderId,
|
||||||
|
'rideTimeStart': DateTime.now().toString(),
|
||||||
|
'status': 'Apply',
|
||||||
|
'passengerToken': passengerToken,
|
||||||
|
'driver_id': driverId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
print("📥 Server Response: $res");
|
||||||
|
|
||||||
|
if (Get.isDialogOpen == true) Get.back(); // إغلاق اللودينج
|
||||||
|
|
||||||
|
// 🔴 فحص النتيجة بدقة (Map أو String)
|
||||||
|
bool isFailure = false;
|
||||||
|
if (res is Map && res['status'] == 'failure') {
|
||||||
|
isFailure = true;
|
||||||
|
} else if (res == 'failure') {
|
||||||
|
isFailure = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFailure) {
|
||||||
|
Get.defaultDialog(
|
||||||
|
title: "تنبيه",
|
||||||
|
middleText: "عذراً، الطلب أخذه سائق آخر.",
|
||||||
|
confirmTextColor: Colors.white,
|
||||||
|
onConfirm: () => Get.back(),
|
||||||
|
textConfirm: "حسناً",
|
||||||
|
);
|
||||||
|
return; // توقف هنا ولا تكمل
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ نجاح -> تجهيز الانتقال
|
||||||
|
|
||||||
|
// حماية من الكراش: التأكد من وجود HomeCaptainController
|
||||||
|
if (!Get.isRegistered<HomeCaptainController>()) {
|
||||||
|
print("♻️ Reviving HomeCaptainController...");
|
||||||
|
Get.put(HomeCaptainController());
|
||||||
|
} else {
|
||||||
|
Get.find<HomeCaptainController>().changeRideId();
|
||||||
|
}
|
||||||
|
|
||||||
|
box.write(BoxName.statusDriverLocation, 'on');
|
||||||
|
box.write(BoxName.rideStatus, 'Apply');
|
||||||
|
|
||||||
|
var rideArgs = _buildRideArgs(data);
|
||||||
|
box.write(BoxName.rideArguments, rideArgs);
|
||||||
|
|
||||||
|
// استخدام offAll لمنع الرجوع لصفحة الطلب
|
||||||
|
Get.offAll(() => PassengerLocationMapPage(), arguments: rideArgs);
|
||||||
|
} catch (e) {
|
||||||
|
if (Get.isDialogOpen == true) Get.back();
|
||||||
|
print("❌ Error in accept process: $e");
|
||||||
|
Get.snackbar("خطأ", "حدث خطأ غير متوقع");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// 5. منطق الرفض (يعمل في الخلفية بدون فتح صفحات)
|
||||||
|
// ==============================================================================
|
||||||
|
Future<void> _processRejectOrder(List<dynamic> data) async {
|
||||||
|
try {
|
||||||
|
final driverId = box.read(BoxName.driverID);
|
||||||
|
String orderId = _getVal(data, 16);
|
||||||
|
|
||||||
|
if (driverId != null && orderId.isNotEmpty) {
|
||||||
|
print("📤 Rejecting Order: $orderId");
|
||||||
|
await CRUD().post(link: AppLink.addDriverOrder, payload: {
|
||||||
|
'driver_id': driverId,
|
||||||
|
'order_id': orderId,
|
||||||
|
'status': 'Refused'
|
||||||
|
});
|
||||||
|
print("✅ Order Rejected Successfully");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("❌ Error rejecting order: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// 6. دوال مساعدة (Helpers)
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
|
Map<String, dynamic> _buildRideArgs(List<dynamic> data) {
|
||||||
|
return {
|
||||||
|
'passengerLocation': '${_getVal(data, 0)},${_getVal(data, 1)}',
|
||||||
|
'passengerDestination': '${_getVal(data, 3)},${_getVal(data, 4)}',
|
||||||
|
'Duration': _getVal(data, 4), // انتبه: تأكد من الإندكس الصحيح للوقت
|
||||||
|
'totalCost': _getVal(data, 26),
|
||||||
|
'Distance': _getVal(data, 5),
|
||||||
|
'name': _getVal(data, 8),
|
||||||
|
'phone': _getVal(data, 10),
|
||||||
|
'email': _getVal(data, 28),
|
||||||
|
'WalletChecked': _getVal(data, 13),
|
||||||
|
'tokenPassenger': _getVal(data, 9),
|
||||||
|
'direction':
|
||||||
|
'https://www.google.com/maps/dir/${_getVal(data, 0)}/${_getVal(data, 1)}/',
|
||||||
|
'DurationToPassenger': _getVal(data, 15),
|
||||||
|
'rideId': _getVal(data, 16),
|
||||||
|
'passengerId': _getVal(data, 7),
|
||||||
|
'driverId': _getVal(data, 18),
|
||||||
|
'durationOfRideValue': _getVal(data, 19),
|
||||||
|
'paymentAmount': _getVal(data, 2),
|
||||||
|
'paymentMethod': _getVal(data, 13) == 'true' ? 'visa' : 'cash',
|
||||||
|
'isHaveSteps': _getVal(data, 20),
|
||||||
|
'step0': _getVal(data, 21),
|
||||||
|
'step1': _getVal(data, 22),
|
||||||
|
'step2': _getVal(data, 23),
|
||||||
|
'step3': _getVal(data, 24),
|
||||||
|
'step4': _getVal(data, 25),
|
||||||
|
'passengerWalletBurc': _getVal(data, 26),
|
||||||
|
'timeOfOrder': DateTime.now().toString(),
|
||||||
|
'totalPassenger': _getVal(data, 2),
|
||||||
|
'carType': _getVal(data, 31),
|
||||||
|
'kazan': _getVal(data, 32),
|
||||||
|
'startNameLocation': _getVal(data, 29),
|
||||||
|
'endNameLocation': _getVal(data, 30),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getVal(List<dynamic> data, int index) {
|
||||||
|
if (data.length > index && data[index] != null) {
|
||||||
|
return data[index].toString();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
void onDidReceiveNotificationResponse(NotificationResponse response) {
|
||||||
|
handleNotificationResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onDidReceiveBackgroundNotificationResponse(
|
||||||
|
NotificationResponse response) {
|
||||||
|
handleNotificationResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================================================================
|
||||||
|
// 7. الدوال القديمة (Old Scheduled Notifications) - لم يتم تغييرها
|
||||||
|
// ==============================================================================
|
||||||
|
|
||||||
void showNotification(
|
void showNotification(
|
||||||
String title, String message, String tone, String payLoad) async {
|
String title, String message, String tone, String payLoad) async {
|
||||||
|
// هذه الدالة القديمة للإشعارات البسيطة (ليس الطلبات)
|
||||||
BigTextStyleInformation bigTextStyleInformation = BigTextStyleInformation(
|
BigTextStyleInformation bigTextStyleInformation = BigTextStyleInformation(
|
||||||
message,
|
message,
|
||||||
contentTitle: title.tr,
|
contentTitle: title.tr,
|
||||||
@@ -71,41 +416,9 @@ class NotificationController extends GetxController {
|
|||||||
'High Importance Notifications',
|
'High Importance Notifications',
|
||||||
importance: Importance.max,
|
importance: Importance.max,
|
||||||
priority: Priority.high,
|
priority: Priority.high,
|
||||||
sound: RawResourceAndroidNotificationSound(tone),
|
sound: RawResourceAndroidNotificationSound(tone.split('.').first),
|
||||||
);
|
);
|
||||||
|
|
||||||
// AndroidNotificationDetails android = AndroidNotificationDetails(
|
|
||||||
// 'high_importance_channel', // Use the same ID as before
|
|
||||||
// 'High Importance Notifications',
|
|
||||||
// importance: Importance.high,
|
|
||||||
// priority: Priority.high,
|
|
||||||
// styleInformation: bigTextStyleInformation,
|
|
||||||
// playSound: true,
|
|
||||||
// sound: RawResourceAndroidNotificationSound(tone),
|
|
||||||
// // audioAttributesUsage: AudioAttributesUsage.alarm,
|
|
||||||
// visibility: NotificationVisibility.public,
|
|
||||||
// autoCancel: false,
|
|
||||||
// color: AppColor.primaryColor,
|
|
||||||
// showProgress: true,
|
|
||||||
// showWhen: true,
|
|
||||||
// ongoing: true,
|
|
||||||
// enableVibration: true,
|
|
||||||
// vibrationPattern: Int64List.fromList([0, 1000, 500, 1000]),
|
|
||||||
// timeoutAfter: 14500,
|
|
||||||
// setAsGroupSummary: true,
|
|
||||||
// subText: message, fullScreenIntent: true,
|
|
||||||
// actions: [
|
|
||||||
// AndroidNotificationAction(
|
|
||||||
// allowGeneratedReplies: true,
|
|
||||||
// 'id',
|
|
||||||
// title.tr,
|
|
||||||
// titleColor: AppColor.blueColor,
|
|
||||||
// showsUserInterface: true,
|
|
||||||
// )
|
|
||||||
// ],
|
|
||||||
// category: AndroidNotificationCategory.message,
|
|
||||||
// );
|
|
||||||
|
|
||||||
DarwinNotificationDetails ios = const DarwinNotificationDetails(
|
DarwinNotificationDetails ios = const DarwinNotificationDetails(
|
||||||
sound: 'default',
|
sound: 'default',
|
||||||
presentAlert: true,
|
presentAlert: true,
|
||||||
@@ -119,126 +432,6 @@ class NotificationController extends GetxController {
|
|||||||
payload: jsonEncode({'title': title, 'data': payLoad}));
|
payload: jsonEncode({'title': title, 'data': payLoad}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void scheduleNotificationAtSpecificTime(
|
|
||||||
String title, String message, String tone, int hour, int minute) async {
|
|
||||||
// Initialize and set Cairo time zone
|
|
||||||
tz.initializeTimeZones();
|
|
||||||
var cairoLocation;
|
|
||||||
if (box.read(BoxName.countryCode).toString() == 'Egypt') {
|
|
||||||
cairoLocation = tz.getLocation('Africa/Cairo');
|
|
||||||
} else {} // todo get for location country
|
|
||||||
|
|
||||||
final AndroidNotificationDetails android = AndroidNotificationDetails(
|
|
||||||
'high_importance_channel',
|
|
||||||
'High Importance Notifications',
|
|
||||||
importance: Importance.max,
|
|
||||||
priority: Priority.high,
|
|
||||||
showWhen: false,
|
|
||||||
sound: RawResourceAndroidNotificationSound(tone),
|
|
||||||
);
|
|
||||||
|
|
||||||
const DarwinNotificationDetails ios = DarwinNotificationDetails(
|
|
||||||
sound: 'default',
|
|
||||||
presentAlert: true,
|
|
||||||
presentBadge: true,
|
|
||||||
presentSound: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
final NotificationDetails details =
|
|
||||||
NotificationDetails(android: android, iOS: ios);
|
|
||||||
|
|
||||||
final now =
|
|
||||||
tz.TZDateTime.now(cairoLocation); // Use Cairo timezone for current time
|
|
||||||
tz.TZDateTime scheduledTime = tz.TZDateTime(
|
|
||||||
cairoLocation, now.year, now.month, now.day, hour, minute);
|
|
||||||
|
|
||||||
// If the scheduled time has already passed for today, schedule it for the next day
|
|
||||||
if (scheduledTime.isBefore(now)) {
|
|
||||||
scheduledTime = scheduledTime.add(const Duration(days: 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
if (await Permission.scheduleExactAlarm.isDenied) {
|
|
||||||
if (await Permission.scheduleExactAlarm.request().isGranted) {
|
|
||||||
print('SCHEDULE_EXACT_ALARM permission granted');
|
|
||||||
} else {
|
|
||||||
print('SCHEDULE_EXACT_ALARM permission denied');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print('Current time: $now');
|
|
||||||
print('Scheduling notification for: $scheduledTime');
|
|
||||||
await _flutterLocalNotificationsPlugin.zonedSchedule(
|
|
||||||
0,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
scheduledTime,
|
|
||||||
details,
|
|
||||||
// androidAllowWhileIdle: true,
|
|
||||||
uiLocalNotificationDateInterpretation:
|
|
||||||
UILocalNotificationDateInterpretation.absoluteTime,
|
|
||||||
matchDateTimeComponents: DateTimeComponents.time,
|
|
||||||
androidScheduleMode:
|
|
||||||
AndroidScheduleMode.alarmClock, // Triggers daily at the same time
|
|
||||||
);
|
|
||||||
print('Notification scheduled successfully');
|
|
||||||
}
|
|
||||||
|
|
||||||
void scheduleNotificationAfter1Minute(
|
|
||||||
String title, String message, String tone) async {
|
|
||||||
final AndroidNotificationDetails android = AndroidNotificationDetails(
|
|
||||||
'high_importance_channel',
|
|
||||||
'High Importance Notifications',
|
|
||||||
importance: Importance.max,
|
|
||||||
priority: Priority.high,
|
|
||||||
showWhen: false,
|
|
||||||
sound: RawResourceAndroidNotificationSound(tone),
|
|
||||||
);
|
|
||||||
|
|
||||||
const DarwinNotificationDetails ios = DarwinNotificationDetails(
|
|
||||||
sound: 'default',
|
|
||||||
presentAlert: true,
|
|
||||||
presentBadge: true,
|
|
||||||
presentSound: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
final NotificationDetails details =
|
|
||||||
NotificationDetails(android: android, iOS: ios);
|
|
||||||
|
|
||||||
// Schedule the notification to be shown after 1 minute
|
|
||||||
Timer.periodic(const Duration(seconds: 15), (timer) async {
|
|
||||||
final now = tz.TZDateTime.now(tz.local);
|
|
||||||
final scheduledTime = now.add(const Duration(seconds: 10));
|
|
||||||
Log.print('scheduledTime: ${scheduledTime}');
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
if (await Permission.scheduleExactAlarm.isDenied) {
|
|
||||||
if (await Permission.scheduleExactAlarm.request().isGranted) {
|
|
||||||
print('SCHEDULE_EXACT_ALARM permission granted');
|
|
||||||
} else {
|
|
||||||
print('SCHEDULE_EXACT_ALARM permission denied');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print('Scheduling notification for: $scheduledTime');
|
|
||||||
await _flutterLocalNotificationsPlugin.zonedSchedule(
|
|
||||||
0,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
scheduledTime,
|
|
||||||
details,
|
|
||||||
uiLocalNotificationDateInterpretation:
|
|
||||||
UILocalNotificationDateInterpretation.absoluteTime,
|
|
||||||
matchDateTimeComponents: DateTimeComponents.time,
|
|
||||||
androidScheduleMode: AndroidScheduleMode.alarmClock,
|
|
||||||
);
|
|
||||||
print('Notification scheduled successfully');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void scheduleNotificationsForSevenDays(
|
void scheduleNotificationsForSevenDays(
|
||||||
String title, String message, String tone) async {
|
String title, String message, String tone) async {
|
||||||
final AndroidNotificationDetails android = AndroidNotificationDetails(
|
final AndroidNotificationDetails android = AndroidNotificationDetails(
|
||||||
@@ -246,7 +439,7 @@ class NotificationController extends GetxController {
|
|||||||
'High Importance Notifications',
|
'High Importance Notifications',
|
||||||
importance: Importance.max,
|
importance: Importance.max,
|
||||||
priority: Priority.high,
|
priority: Priority.high,
|
||||||
sound: RawResourceAndroidNotificationSound(tone),
|
sound: RawResourceAndroidNotificationSound(tone.split('.').first),
|
||||||
);
|
);
|
||||||
|
|
||||||
const DarwinNotificationDetails ios = DarwinNotificationDetails(
|
const DarwinNotificationDetails ios = DarwinNotificationDetails(
|
||||||
@@ -259,35 +452,24 @@ class NotificationController extends GetxController {
|
|||||||
final NotificationDetails details =
|
final NotificationDetails details =
|
||||||
NotificationDetails(android: android, iOS: ios);
|
NotificationDetails(android: android, iOS: ios);
|
||||||
|
|
||||||
// Check for the exact alarm permission on Android 12 and above
|
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
if (await Permission.scheduleExactAlarm.isDenied) {
|
if (await Permission.scheduleExactAlarm.isDenied) {
|
||||||
if (await Permission.scheduleExactAlarm.request().isGranted) {
|
await Permission.scheduleExactAlarm.request();
|
||||||
print('SCHEDULE_EXACT_ALARM permission granted');
|
|
||||||
} else {
|
|
||||||
print('SCHEDULE_EXACT_ALARM permission denied');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule notifications for the next 7 days
|
|
||||||
for (int day = 0; day < 7; day++) {
|
for (int day = 0; day < 7; day++) {
|
||||||
// List of notification times
|
|
||||||
final notificationTimes = [
|
final notificationTimes = [
|
||||||
{'hour': 8, 'minute': 0, 'id': day * 1000 + 1}, // 8:00 AM
|
{'hour': 8, 'minute': 0, 'id': day * 1000 + 1},
|
||||||
{'hour': 15, 'minute': 0, 'id': day * 1000 + 2}, // 3:00 PM
|
{'hour': 15, 'minute': 0, 'id': day * 1000 + 2},
|
||||||
{'hour': 20, 'minute': 0, 'id': day * 1000 + 3}, // 8:00 PM
|
{'hour': 20, 'minute': 0, 'id': day * 1000 + 3},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (var time in notificationTimes) {
|
for (var time in notificationTimes) {
|
||||||
final notificationId = time['id'] as int;
|
final notificationId = time['id'] as int;
|
||||||
|
|
||||||
// Check if this notification ID is already stored
|
|
||||||
bool isScheduled = box.read('notification_$notificationId') ?? false;
|
bool isScheduled = box.read('notification_$notificationId') ?? false;
|
||||||
|
|
||||||
if (!isScheduled) {
|
if (!isScheduled) {
|
||||||
// Schedule the notification if not already scheduled
|
|
||||||
await _scheduleNotificationForTime(
|
await _scheduleNotificationForTime(
|
||||||
day,
|
day,
|
||||||
time['hour'] as int,
|
time['hour'] as int,
|
||||||
@@ -297,16 +479,19 @@ class NotificationController extends GetxController {
|
|||||||
details,
|
details,
|
||||||
notificationId,
|
notificationId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mark this notification ID as scheduled in GetStorage
|
|
||||||
box.write('notification_$notificationId', true);
|
box.write('notification_$notificationId', true);
|
||||||
} else {
|
}
|
||||||
print('Notification with ID $notificationId is already scheduled.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print('Notifications scheduled successfully for the next 7 days');
|
// ==============================================================================
|
||||||
|
// دالة حذف إشعار الطلب (تستدعى عند أخذ الطلب من سائق آخر)
|
||||||
|
// ==============================================================================
|
||||||
|
Future<void> cancelOrderNotification() async {
|
||||||
|
// 1001 هو نفس الآيدي الذي استخدمناه عند عرض الإشعار
|
||||||
|
await _flutterLocalNotificationsPlugin.cancel(1001);
|
||||||
|
print("🗑️ Order Notification Cancelled (Taken by another driver)");
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _scheduleNotificationForTime(
|
Future<void> _scheduleNotificationForTime(
|
||||||
@@ -318,30 +503,26 @@ class NotificationController extends GetxController {
|
|||||||
NotificationDetails details,
|
NotificationDetails details,
|
||||||
int notificationId,
|
int notificationId,
|
||||||
) async {
|
) async {
|
||||||
// Initialize and set Cairo timezone
|
|
||||||
tz.initializeTimeZones();
|
tz.initializeTimeZones();
|
||||||
var cairoLocation = tz.getLocation('Africa/Cairo');
|
var cairoLocation =
|
||||||
|
tz.getLocation('Africa/Cairo'); // تأكد من المنطقة الزمنية
|
||||||
|
|
||||||
final now = tz.TZDateTime.now(cairoLocation);
|
final now = tz.TZDateTime.now(cairoLocation);
|
||||||
tz.TZDateTime scheduledDate = tz.TZDateTime(
|
tz.TZDateTime scheduledDate = tz.TZDateTime(
|
||||||
cairoLocation,
|
cairoLocation,
|
||||||
now.year,
|
now.year,
|
||||||
now.month,
|
now.month,
|
||||||
now.day + dayOffset, // Add offset to schedule for the next days
|
now.day + dayOffset,
|
||||||
hour,
|
hour,
|
||||||
minute,
|
minute,
|
||||||
);
|
);
|
||||||
|
|
||||||
// If the scheduled time is in the past, move it to the next day
|
|
||||||
if (scheduledDate.isBefore(now)) {
|
if (scheduledDate.isBefore(now)) {
|
||||||
scheduledDate = scheduledDate.add(Duration(days: 1));
|
scheduledDate = scheduledDate.add(const Duration(days: 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
print('Current time (Cairo): $now');
|
|
||||||
print('Scheduling notification for: $scheduledDate');
|
|
||||||
|
|
||||||
await _flutterLocalNotificationsPlugin.zonedSchedule(
|
await _flutterLocalNotificationsPlugin.zonedSchedule(
|
||||||
notificationId, // Unique ID for each notification
|
notificationId,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
scheduledDate,
|
scheduledDate,
|
||||||
@@ -349,243 +530,7 @@ class NotificationController extends GetxController {
|
|||||||
androidScheduleMode: AndroidScheduleMode.exact,
|
androidScheduleMode: AndroidScheduleMode.exact,
|
||||||
uiLocalNotificationDateInterpretation:
|
uiLocalNotificationDateInterpretation:
|
||||||
UILocalNotificationDateInterpretation.absoluteTime,
|
UILocalNotificationDateInterpretation.absoluteTime,
|
||||||
matchDateTimeComponents:
|
matchDateTimeComponents: null,
|
||||||
null, // Don't repeat automatically; we handle 7 days manually
|
|
||||||
);
|
);
|
||||||
|
|
||||||
print('Notification scheduled successfully for: $scheduledDate');
|
|
||||||
}
|
|
||||||
|
|
||||||
void scheduleNotificationEvery10Hours(
|
|
||||||
String title, String message, String tone) async {
|
|
||||||
final AndroidNotificationDetails android = AndroidNotificationDetails(
|
|
||||||
'high_importance_channel',
|
|
||||||
'High Importance Notifications',
|
|
||||||
importance: Importance.max,
|
|
||||||
priority: Priority.high,
|
|
||||||
showWhen: false,
|
|
||||||
sound: RawResourceAndroidNotificationSound(tone),
|
|
||||||
);
|
|
||||||
|
|
||||||
const DarwinNotificationDetails ios = DarwinNotificationDetails(
|
|
||||||
sound: 'default',
|
|
||||||
presentAlert: true,
|
|
||||||
presentBadge: true,
|
|
||||||
presentSound: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
final NotificationDetails details =
|
|
||||||
NotificationDetails(android: android, iOS: ios);
|
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
if (await Permission.scheduleExactAlarm.isDenied) {
|
|
||||||
if (await Permission.scheduleExactAlarm.request().isGranted) {
|
|
||||||
print('SCHEDULE_EXACT_ALARM permission granted');
|
|
||||||
} else {
|
|
||||||
print('SCHEDULE_EXACT_ALARM permission denied');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer.periodic(const Duration(hours: 10), (timer) async {
|
|
||||||
final now = tz.TZDateTime.now(tz.local);
|
|
||||||
final scheduledTime = now.add(const Duration(minutes: 10));
|
|
||||||
|
|
||||||
print('Scheduling notification for: $scheduledTime');
|
|
||||||
await _flutterLocalNotificationsPlugin.zonedSchedule(
|
|
||||||
0,
|
|
||||||
title.tr,
|
|
||||||
message.tr,
|
|
||||||
scheduledTime,
|
|
||||||
details,
|
|
||||||
// androidAllowWhileIdle: true,
|
|
||||||
uiLocalNotificationDateInterpretation:
|
|
||||||
UILocalNotificationDateInterpretation.absoluteTime,
|
|
||||||
matchDateTimeComponents: DateTimeComponents.time,
|
|
||||||
androidScheduleMode: AndroidScheduleMode.alarmClock,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
print('Notifications scheduled every 5 seconds');
|
|
||||||
}
|
|
||||||
|
|
||||||
void showTimerNotification(String title, String message, String tone) async {
|
|
||||||
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
|
||||||
FlutterLocalNotificationsPlugin();
|
|
||||||
|
|
||||||
// Setup Android notification
|
|
||||||
final AndroidNotificationDetails android = AndroidNotificationDetails(
|
|
||||||
'high_importance_channel',
|
|
||||||
'High Importance Notifications',
|
|
||||||
importance: Importance.max,
|
|
||||||
priority: Priority.high,
|
|
||||||
showWhen: false,
|
|
||||||
sound: RawResourceAndroidNotificationSound(
|
|
||||||
tone), // tone without the file extension
|
|
||||||
);
|
|
||||||
|
|
||||||
// Setup iOS notification
|
|
||||||
const DarwinNotificationDetails ios = DarwinNotificationDetails(
|
|
||||||
sound: 'default',
|
|
||||||
presentAlert: true,
|
|
||||||
presentBadge: true,
|
|
||||||
presentSound: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
final NotificationDetails details =
|
|
||||||
NotificationDetails(android: android, iOS: ios);
|
|
||||||
|
|
||||||
// Request permission on Android
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
if (await Permission.scheduleExactAlarm.isDenied) {
|
|
||||||
if (await Permission.scheduleExactAlarm.request().isGranted) {
|
|
||||||
print('SCHEDULE_EXACT_ALARM permission granted');
|
|
||||||
} else {
|
|
||||||
print('SCHEDULE_EXACT_ALARM permission denied');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timer duration (e.g., 120 seconds countdown)
|
|
||||||
int countdown = 12;
|
|
||||||
|
|
||||||
// Display the notification initially with the full countdown time
|
|
||||||
|
|
||||||
// Timer to update the notification every second
|
|
||||||
Timer.periodic(const Duration(seconds: 1), (timer) async {
|
|
||||||
// if (countdown > 0) {
|
|
||||||
// Update the existing notification with the updated countdown
|
|
||||||
|
|
||||||
// Decrease the countdown by 1
|
|
||||||
countdown--;
|
|
||||||
// } else {
|
|
||||||
// // Cancel the timer when the countdown reaches zero
|
|
||||||
// timer.cancel();
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
await flutterLocalNotificationsPlugin.show(
|
|
||||||
0,
|
|
||||||
title,
|
|
||||||
'$message Remaining: $countdown seconds', // Initial message
|
|
||||||
details,
|
|
||||||
);
|
|
||||||
print('Notification will update every second');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback when the notification is tapped
|
|
||||||
void onDidReceiveNotificationResponse(NotificationResponse response) {
|
|
||||||
handleNotificationResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback when the notification is tapped while the app is in the background
|
|
||||||
void onDidReceiveBackgroundNotificationResponse(
|
|
||||||
NotificationResponse response) {
|
|
||||||
handleNotificationResponse(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle notification response for both foreground and background
|
|
||||||
void handleNotificationResponse(NotificationResponse response) {
|
|
||||||
print('Notification tapped!');
|
|
||||||
Log.print('response.payload: ${response.payload}');
|
|
||||||
if (response.payload != null) {
|
|
||||||
print('Notification payload: ${response.payload}');
|
|
||||||
var payloadData = jsonDecode(response.payload.toString());
|
|
||||||
|
|
||||||
if (payloadData is Map<String, dynamic>) {
|
|
||||||
String title = payloadData['title'];
|
|
||||||
var data = payloadData['data'];
|
|
||||||
|
|
||||||
switch (title) {
|
|
||||||
case 'Order':
|
|
||||||
_handleOrderNotification(data);
|
|
||||||
break;
|
|
||||||
case 'OrderSpeed':
|
|
||||||
_handleOrderSpeedNotification(data);
|
|
||||||
break;
|
|
||||||
case 'ADS':
|
|
||||||
_handleADSNotification();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Log.print('Unknown notification type');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.print('Invalid payload format');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.print('Payload is null');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleOrderNotification(dynamic data) {
|
|
||||||
if (data is String) {
|
|
||||||
var orderData = jsonDecode(data);
|
|
||||||
if (orderData is List && orderData.length == 34) {
|
|
||||||
//closeOverLay();
|
|
||||||
Get.put(HomeCaptainController()).changeRideId();
|
|
||||||
Get.to(() => OrderRequestPage(), arguments: {'myListString': data});
|
|
||||||
} else {
|
|
||||||
Log.print('Invalid order data');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.print('Invalid order payload');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleOrderSpeedNotification(dynamic data) {
|
|
||||||
if (data is String) {
|
|
||||||
var orderData = jsonDecode(data);
|
|
||||||
if (orderData is List && orderData.length == 34) {
|
|
||||||
//closeOverLay();
|
|
||||||
Get.put(HomeCaptainController()).changeRideId();
|
|
||||||
Get.to(() => OrderRequestPage(), arguments: {'myListString': data});
|
|
||||||
} else {
|
|
||||||
Log.print('Invalid order data');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.print('Invalid order payload');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleADSNotification() {
|
|
||||||
// var orderData = jsonDecode(data);
|
|
||||||
//closeOverLay();
|
|
||||||
Get.to(
|
|
||||||
() => const NotificationCaptain(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onDidReceiveLocalNotification(
|
|
||||||
int id, String? title, String? body, String? payload) async {
|
|
||||||
// display a dialog with the notification details, tap ok to go to another page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class NotificationController1 extends GetxController {
|
|
||||||
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
|
|
||||||
FlutterLocalNotificationsPlugin();
|
|
||||||
|
|
||||||
// Initializes the local notifications plugin
|
|
||||||
Future<void> initNotifications() async {
|
|
||||||
const AndroidInitializationSettings android =
|
|
||||||
AndroidInitializationSettings('@mipmap/launcher_icon');
|
|
||||||
const InitializationSettings initializationSettings =
|
|
||||||
InitializationSettings(android: android);
|
|
||||||
await _flutterLocalNotificationsPlugin.initialize(initializationSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Displays a notification with the given title and message
|
|
||||||
void showNotification(
|
|
||||||
String title, String message, String tone, String payLoad) async {
|
|
||||||
AndroidNotificationDetails android = AndroidNotificationDetails(
|
|
||||||
'your channel id', 'your channel name',
|
|
||||||
importance: Importance.max,
|
|
||||||
priority: Priority.high,
|
|
||||||
showWhen: false,
|
|
||||||
sound: RawResourceAndroidNotificationSound(tone));
|
|
||||||
|
|
||||||
NotificationDetails details = NotificationDetails(android: android);
|
|
||||||
await _flutterLocalNotificationsPlugin.show(0, title, message, details);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
lib/controller/firebase/notification_service.dart
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:get/get.dart'; // للترجمة .tr
|
||||||
|
|
||||||
|
class NotificationService {
|
||||||
|
static const String _serverUrl =
|
||||||
|
'https://api.intaleq.xyz/intaleq/ride/firebase/send_fcm.php';
|
||||||
|
|
||||||
|
static Future<void> sendNotification({
|
||||||
|
required String target,
|
||||||
|
required String title,
|
||||||
|
required String body,
|
||||||
|
required String category, // إلزامي للتصنيف
|
||||||
|
String? tone,
|
||||||
|
List<String>? driverList,
|
||||||
|
bool isTopic = false,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// 1. تجهيز البيانات المخصصة (Data Payload)
|
||||||
|
Map<String, dynamic> customData = {};
|
||||||
|
|
||||||
|
customData['category'] = category;
|
||||||
|
|
||||||
|
// إذا كان هناك قائمة سائقين/ركاب، نضعها هنا
|
||||||
|
if (driverList != null && driverList.isNotEmpty) {
|
||||||
|
// نرسلها كـ JSON String لأن FCM v1 يدعم String Values فقط في الـ data
|
||||||
|
customData['driverList'] = jsonEncode(driverList);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. تجهيز الطلب الرئيسي للسيرفر
|
||||||
|
final Map<String, dynamic> requestPayload = {
|
||||||
|
'target': target,
|
||||||
|
'title': title,
|
||||||
|
'body': body,
|
||||||
|
'isTopic': isTopic,
|
||||||
|
'data':
|
||||||
|
customData, // 🔥🔥 التغيير الجوهري: وضعنا البيانات داخل "data" 🔥🔥
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tone != null) {
|
||||||
|
requestPayload['tone'] = tone;
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse(_serverUrl),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=UTF-8',
|
||||||
|
},
|
||||||
|
body: jsonEncode(requestPayload),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
print('✅ Notification sent successfully.');
|
||||||
|
// print('Response: ${response.body}');
|
||||||
|
} else {
|
||||||
|
print('❌ Failed to send notification. Code: ${response.statusCode}');
|
||||||
|
print('Error Body: ${response.body}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Error sending notification: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
164
lib/controller/functions/background_service.dart
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_background_service/flutter_background_service.dart';
|
||||||
|
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||||
|
import 'package:socket_io_client/socket_io_client.dart' as IO;
|
||||||
|
import 'package:flutter_overlay_window/flutter_overlay_window.dart' as Overlay;
|
||||||
|
import 'package:get_storage/get_storage.dart';
|
||||||
|
import '../../constant/box_name.dart';
|
||||||
|
|
||||||
|
const String notificationChannelId = 'driver_service_channel';
|
||||||
|
const int notificationId = 888;
|
||||||
|
const String notificationIcon = '@mipmap/launcher_icon';
|
||||||
|
|
||||||
|
@pragma('vm:entry-point')
|
||||||
|
Future<bool> onStart(ServiceInstance service) async {
|
||||||
|
DartPluginRegistrant.ensureInitialized();
|
||||||
|
|
||||||
|
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||||
|
FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
await GetStorage.init();
|
||||||
|
final box = GetStorage();
|
||||||
|
|
||||||
|
IO.Socket? socket;
|
||||||
|
String driverId = box.read(BoxName.driverID) ?? '';
|
||||||
|
String token = box.read(BoxName.tokenDriver) ?? '';
|
||||||
|
|
||||||
|
if (driverId.isNotEmpty) {
|
||||||
|
socket = IO.io(
|
||||||
|
'https://location.intaleq.xyz',
|
||||||
|
IO.OptionBuilder()
|
||||||
|
.setTransports(['websocket'])
|
||||||
|
.disableAutoConnect()
|
||||||
|
.setQuery({'driver_id': driverId, 'token': token})
|
||||||
|
.setReconnectionAttempts(double.infinity)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
socket.connect();
|
||||||
|
|
||||||
|
socket.onConnect((_) {
|
||||||
|
print("✅ Background Service: Socket Connected!");
|
||||||
|
if (service is AndroidServiceInstance) {
|
||||||
|
flutterLocalNotificationsPlugin.show(
|
||||||
|
notificationId,
|
||||||
|
'أنت متصل الآن',
|
||||||
|
'بانتظار الطلبات...',
|
||||||
|
const NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
notificationChannelId,
|
||||||
|
'خدمة السائق',
|
||||||
|
icon: notificationIcon,
|
||||||
|
ongoing: true,
|
||||||
|
importance: Importance.low,
|
||||||
|
priority: Priority.low,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('new_ride_request', (data) async {
|
||||||
|
print("🔔 Background Service: Received new_ride_request");
|
||||||
|
|
||||||
|
// 🔥 قراءة حالة التطبيق مباشرة قبل العرض
|
||||||
|
await GetStorage.init(); // تأكد من تحديث البيانات
|
||||||
|
final box = GetStorage();
|
||||||
|
bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false;
|
||||||
|
|
||||||
|
// 🔥 Check إضافي: هل الـ Overlay مفتوح بالفعل؟
|
||||||
|
bool overlayActive = await Overlay.FlutterOverlayWindow.isActive();
|
||||||
|
|
||||||
|
if (isAppInForeground || overlayActive) {
|
||||||
|
print("🛑 App is FOREGROUND or Overlay already shown. Skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// عرض الـ Overlay
|
||||||
|
print("🚀 App is BACKGROUND. Showing Overlay...");
|
||||||
|
try {
|
||||||
|
await Overlay.FlutterOverlayWindow.showOverlay(
|
||||||
|
enableDrag: true,
|
||||||
|
overlayTitle: "طلب جديد",
|
||||||
|
overlayContent: "لديك طلب جديد وصل للتو!",
|
||||||
|
flag: OverlayFlag.focusPointer,
|
||||||
|
positionGravity: PositionGravity.auto,
|
||||||
|
height: WindowSize.matchParent,
|
||||||
|
width: WindowSize.matchParent,
|
||||||
|
startPosition: const OverlayPosition(0, -30),
|
||||||
|
);
|
||||||
|
await Overlay.FlutterOverlayWindow.shareData(data);
|
||||||
|
} catch (e) {
|
||||||
|
print("Overlay Error: $e");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
service.on('stopService').listen((event) {
|
||||||
|
socket?.disconnect();
|
||||||
|
service.stopSelf();
|
||||||
|
});
|
||||||
|
|
||||||
|
Timer.periodic(const Duration(seconds: 30), (timer) async {
|
||||||
|
if (service is AndroidServiceInstance) {
|
||||||
|
if (await service.isForegroundService()) {
|
||||||
|
flutterLocalNotificationsPlugin.show(
|
||||||
|
notificationId,
|
||||||
|
'خدمة السائق نشطة',
|
||||||
|
'بانتظار الطلبات...',
|
||||||
|
const NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
notificationChannelId,
|
||||||
|
'خدمة السائق',
|
||||||
|
icon: notificationIcon,
|
||||||
|
ongoing: true,
|
||||||
|
importance: Importance.low,
|
||||||
|
priority: Priority.low,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BackgroundServiceHelper {
|
||||||
|
static Future<void> initialize() async {
|
||||||
|
final service = FlutterBackgroundService();
|
||||||
|
|
||||||
|
await service.configure(
|
||||||
|
androidConfiguration: AndroidConfiguration(
|
||||||
|
onStart: onStart,
|
||||||
|
autoStart: false,
|
||||||
|
isForegroundMode: true,
|
||||||
|
notificationChannelId: notificationChannelId,
|
||||||
|
initialNotificationTitle: 'تطبيق السائق',
|
||||||
|
initialNotificationContent: 'تجهيز الخدمة...',
|
||||||
|
foregroundServiceNotificationId: notificationId,
|
||||||
|
),
|
||||||
|
iosConfiguration: IosConfiguration(
|
||||||
|
autoStart: false,
|
||||||
|
onForeground: onStart,
|
||||||
|
onBackground: onStart,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> startService() async {
|
||||||
|
final service = FlutterBackgroundService();
|
||||||
|
if (!await service.isRunning()) {
|
||||||
|
await service.startService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> stopService() async {
|
||||||
|
final service = FlutterBackgroundService();
|
||||||
|
service.invoke("stopService");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:jwt_decoder/jwt_decoder.dart';
|
import 'package:jwt_decoder/jwt_decoder.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:sefer_driver/controller/functions/encrypt_decrypt.dart';
|
||||||
import 'package:sefer_driver/controller/functions/network/net_guard.dart';
|
import 'package:sefer_driver/controller/functions/network/net_guard.dart';
|
||||||
import 'package:secure_string_operations/secure_string_operations.dart';
|
import 'package:secure_string_operations/secure_string_operations.dart';
|
||||||
import 'package:sefer_driver/constant/box_name.dart';
|
import 'package:sefer_driver/constant/box_name.dart';
|
||||||
@@ -10,18 +13,18 @@ import 'package:sefer_driver/main.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:sefer_driver/env/env.dart';
|
import 'package:sefer_driver/env/env.dart';
|
||||||
|
import 'package:sefer_driver/print.dart';
|
||||||
|
|
||||||
import '../../constant/api_key.dart';
|
import '../../constant/api_key.dart';
|
||||||
import '../../constant/char_map.dart';
|
import '../../constant/char_map.dart';
|
||||||
import '../../constant/info.dart';
|
import '../../constant/info.dart';
|
||||||
import '../../views/widgets/error_snakbar.dart';
|
import '../../views/widgets/error_snakbar.dart';
|
||||||
import '../../print.dart';
|
|
||||||
import 'gemeni.dart';
|
import 'gemeni.dart';
|
||||||
import 'network/connection_check.dart';
|
|
||||||
import 'upload_image.dart';
|
import 'upload_image.dart';
|
||||||
|
|
||||||
class CRUD {
|
class CRUD {
|
||||||
final NetGuard _netGuard = NetGuard();
|
final NetGuard _netGuard = NetGuard();
|
||||||
|
final _client = http.Client();
|
||||||
|
|
||||||
/// Stores the signature of the last logged error to prevent duplicates.
|
/// Stores the signature of the last logged error to prevent duplicates.
|
||||||
static String _lastErrorSignature = '';
|
static String _lastErrorSignature = '';
|
||||||
@@ -40,32 +43,24 @@ class CRUD {
|
|||||||
static Future<void> addError(
|
static Future<void> addError(
|
||||||
String error, String details, String where) async {
|
String error, String details, String where) async {
|
||||||
try {
|
try {
|
||||||
// Create a unique signature for the current error
|
|
||||||
final currentErrorSignature = '$where-$error';
|
final currentErrorSignature = '$where-$error';
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
|
|
||||||
// Check if the same error occurred recently
|
|
||||||
if (currentErrorSignature == _lastErrorSignature &&
|
if (currentErrorSignature == _lastErrorSignature &&
|
||||||
now.difference(_lastErrorTimestamp) < _errorLogDebounceDuration) {
|
now.difference(_lastErrorTimestamp) < _errorLogDebounceDuration) {
|
||||||
// If it's the same error within the debounce duration, ignore it.
|
|
||||||
print("Debounced a duplicate error: $error");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the signature and timestamp for the new error
|
|
||||||
_lastErrorSignature = currentErrorSignature;
|
_lastErrorSignature = currentErrorSignature;
|
||||||
_lastErrorTimestamp = now;
|
_lastErrorTimestamp = now;
|
||||||
|
|
||||||
// Get user information for the error log
|
|
||||||
final userId =
|
final userId =
|
||||||
box.read(BoxName.driverID) ?? box.read(BoxName.passengerID);
|
box.read(BoxName.driverID) ?? box.read(BoxName.passengerID);
|
||||||
final userType =
|
final userType =
|
||||||
box.read(BoxName.driverID) != null ? 'Driver' : 'passenger';
|
box.read(BoxName.driverID) != null ? 'Driver' : 'Passenger';
|
||||||
final phone = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver);
|
final phone = box.read(BoxName.phone) ?? box.read(BoxName.phoneDriver);
|
||||||
|
|
||||||
// Send the error data to the server
|
// Fire-and-forget call to prevent infinite loops if the logger itself fails.
|
||||||
// Note: This is a fire-and-forget call. We don't await it or handle its response
|
|
||||||
// to prevent an infinite loop if the addError endpoint itself is failing.
|
|
||||||
CRUD().post(
|
CRUD().post(
|
||||||
link: AppLink.addError,
|
link: AppLink.addError,
|
||||||
payload: {
|
payload: {
|
||||||
@@ -73,134 +68,187 @@ class CRUD {
|
|||||||
'userId': userId.toString(),
|
'userId': userId.toString(),
|
||||||
'userType': userType,
|
'userType': userType,
|
||||||
'phone': phone.toString(),
|
'phone': phone.toString(),
|
||||||
'device': where, // The location of the error
|
'device': where,
|
||||||
'details': details, // The detailed stack trace or context
|
'details': details,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
// If logging the error itself fails, print to the console to avoid infinite loops.
|
|
||||||
print("Failed to log error to server: $e");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Centralized private method to handle all API requests.
|
||||||
|
/// Includes retry logic, network checking, and standardized error handling.
|
||||||
|
// --- تعديل 1: دالة _makeRequest محسنة للإنترنت الضعيف ---
|
||||||
Future<dynamic> _makeRequest({
|
Future<dynamic> _makeRequest({
|
||||||
required String link,
|
required String link,
|
||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
required Map<String, String> headers,
|
required Map<String, String> headers,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
// 🟢 زيادة الوقت للسماح بالشبكات البطيئة (سوريا)
|
||||||
// 1. Wrap the http.post call directly with HttpRetry.sendWithRetry.
|
const connectTimeout = Duration(seconds: 20); // رفعنا الوقت من 6 لـ 20
|
||||||
// It will attempt the request immediately and retry on transient errors.
|
const receiveTimeout = Duration(seconds: 40); // رفعنا الوقت من 10 لـ 40
|
||||||
var response = await HttpRetry.sendWithRetry(
|
|
||||||
() {
|
|
||||||
var url = Uri.parse(link);
|
|
||||||
return http.post(
|
|
||||||
url,
|
|
||||||
body: payload,
|
|
||||||
headers: headers,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
// Optional: you can customize retry behavior for each call
|
|
||||||
maxRetries: 3,
|
|
||||||
timeout: const Duration(seconds: 15),
|
|
||||||
);
|
|
||||||
// Log.print('response: ${response.body}');
|
|
||||||
// Log.print('request: ${response.request}');
|
|
||||||
// Log.print('payload: ${payload}');
|
|
||||||
// ✅ All your existing logic for handling server responses remains the same.
|
|
||||||
// This part is only reached if the network request itself was successful.
|
|
||||||
|
|
||||||
// Handle successful response (200 OK)
|
Future<http.Response> doPost() {
|
||||||
if (response.statusCode == 200) {
|
final url = Uri.parse(link);
|
||||||
|
// نستخدم _client إذا كان معرفاً، أو ننشئ واحداً جديداً مع إغلاقه لاحقاً
|
||||||
|
// لضمان عدم حدوث مشاكل، سنستخدم http.post المباشر كما في النسخة المستقرة لديك
|
||||||
|
// ولكن مع timeout أطول
|
||||||
|
return http
|
||||||
|
.post(url, body: payload, headers: headers)
|
||||||
|
.timeout(connectTimeout + receiveTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Response response = http.Response('', 500); // Default initialization
|
||||||
|
|
||||||
|
// 🟢 محاولة إعادة الاتصال (Retry) حتى 3 مرات
|
||||||
|
int attempts = 0;
|
||||||
|
while (attempts < 3) {
|
||||||
try {
|
try {
|
||||||
var jsonData = jsonDecode(response.body);
|
attempts++;
|
||||||
if (jsonData['status'] == 'success') {
|
response = await doPost();
|
||||||
return jsonData; // Return the full JSON object on success
|
|
||||||
} else {
|
// إذا نجح الاتصال، نخرج من الحلقة ونعالج الرد
|
||||||
if (jsonData['status'] == 'failure') {
|
break;
|
||||||
// return 'failure';
|
} on SocketException catch (_) {
|
||||||
} else {
|
if (attempts >= 3) {
|
||||||
addError(
|
_netGuard.notifyOnce((title, msg) => mySnackeBarError(msg));
|
||||||
'API Logic Error: ${jsonData['status']}',
|
return 'no_internet';
|
||||||
'Response: ${response.body}',
|
|
||||||
'CRUD._makeRequest - $link',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return jsonData['status']; // Return the specific status string
|
// انتظار بسيط قبل المحاولة التالية (مهم جداً للشبكات المتقطعة)
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
} on TimeoutException catch (_) {
|
||||||
|
if (attempts >= 3) return 'failure';
|
||||||
|
// لا ننتظر هنا، نعيد المحاولة فوراً
|
||||||
|
} catch (e) {
|
||||||
|
// إذا كان الخطأ هو errno = 9 (Bad file descriptor) نعيد المحاولة
|
||||||
|
if (e.toString().contains('errno = 9') && attempts < 3) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
} catch (e, stackTrace) {
|
// أخطاء أخرى لا يمكن تجاوزها
|
||||||
addError(
|
addError(
|
||||||
'JSON Decode Error: $e',
|
'HTTP Exception: $e', 'Try: $attempts', 'CRUD._makeRequest $link');
|
||||||
'Response Body: ${response.body}\nStack Trace: $stackTrace',
|
|
||||||
'CRUD._makeRequest - $link',
|
|
||||||
);
|
|
||||||
return 'failure';
|
return 'failure';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle Unauthorized (401)
|
|
||||||
else if (response.statusCode == 401) {
|
// --- معالجة الرد (كما هي في كودك) ---
|
||||||
|
// ملاحظة: المتغير response هنا قد يكون غير معرف (null) إذا فشلت كل المحاولات
|
||||||
|
// لكن بسبب الـ return داخل الـ catch، لن نصل هنا إلا بوجود response
|
||||||
|
|
||||||
|
// الحل الآمن لضمان وجود response قبل استخدامه:
|
||||||
|
try {
|
||||||
|
// إعادة تعريف response لضمان عدم حدوث خطأ null safety في المحرر
|
||||||
|
// (في المنطق الفعلي لن نصل هنا إلا ومعنا response)
|
||||||
|
if (attempts > 3) return 'failure';
|
||||||
|
|
||||||
|
final sc = response.statusCode; // استخدمنا ! لأننا متأكدين
|
||||||
|
final body = response.body;
|
||||||
|
|
||||||
|
if (sc >= 200 && sc < 300) {
|
||||||
|
try {
|
||||||
|
final jsonData = jsonDecode(body);
|
||||||
|
return jsonData;
|
||||||
|
} catch (e, st) {
|
||||||
|
addError('JSON Decode Error', 'Body: $body\n$st',
|
||||||
|
'CRUD._makeRequest $link');
|
||||||
|
return 'failure';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sc == 401) {
|
||||||
|
return 'token_expired';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sc >= 500) {
|
||||||
|
addError(
|
||||||
|
'Server 5xx', 'SC: $sc\nBody: $body', 'CRUD._makeRequest $link');
|
||||||
|
return 'failure';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'failure';
|
||||||
|
} catch (e) {
|
||||||
|
return 'failure';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- تعديل 2: دالة get (كما طلبت: بوست + إرجاع النص الخام) ---
|
||||||
|
// أبقيتها كما هي في كودك الأصلي تماماً، فقط حسنت الـ Timeout
|
||||||
|
Future<dynamic> get({
|
||||||
|
required String link,
|
||||||
|
Map<String, dynamic>? payload,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
var url = Uri.parse(link);
|
||||||
|
|
||||||
|
// 🟢 إضافة timeout هنا أيضاً
|
||||||
|
var response = await http.post(
|
||||||
|
url,
|
||||||
|
body: payload,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
'Authorization':
|
||||||
|
'Bearer ${r(box.read(BoxName.jwt)).toString().split(Env.addd)[0]}'
|
||||||
|
},
|
||||||
|
).timeout(const Duration(seconds: 40)); // وقت كافٍ للشبكات الضعيفة
|
||||||
|
Log.print('response: ${response.body}');
|
||||||
|
Log.print('response: ${response.request}');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
// المنطق الخاص بك: إرجاع الـ body كاملاً كنص (String)
|
||||||
|
// لأنك تريد عمل jsonDecode لاحقاً في المكان الذي استدعى الدالة
|
||||||
|
// أو التحقق من status: success داخلياً
|
||||||
|
|
||||||
|
// ملاحظة: في كودك الأصلي كنت تفحص jsonDecode هنا وتعود بـ response.body
|
||||||
|
// سأبقيها كما هي:
|
||||||
|
var jsonData = jsonDecode(response.body);
|
||||||
|
if (jsonData['status'] == 'success') {
|
||||||
|
return response.body; // إرجاع النص الخام
|
||||||
|
}
|
||||||
|
return jsonData['status'];
|
||||||
|
} else if (response.statusCode == 401) {
|
||||||
var jsonData = jsonDecode(response.body);
|
var jsonData = jsonDecode(response.body);
|
||||||
if (jsonData['error'] == 'Token expired') {
|
if (jsonData['error'] == 'Token expired') {
|
||||||
await Get.put(LoginDriverController()).getJWT();
|
await Get.put(LoginDriverController()).getJWT();
|
||||||
return 'token_expired';
|
return 'token_expired';
|
||||||
} else {
|
} else {
|
||||||
addError(
|
// addError('Unauthorized: ${jsonData['error']}', 'crud().get - 401',
|
||||||
'Unauthorized Error: ${jsonData['error']}',
|
// url.toString());
|
||||||
'Status Code: 401',
|
|
||||||
'CRUD._makeRequest - $link',
|
|
||||||
);
|
|
||||||
return 'failure';
|
return 'failure';
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
// Handle all other non-successful status codes
|
addError('Non-200: ${response.statusCode}', 'crud().get - Other',
|
||||||
else {
|
url.toString());
|
||||||
addError(
|
|
||||||
'HTTP Error',
|
|
||||||
'Status Code: ${response.statusCode}\nResponse Body: ${response.body}',
|
|
||||||
'CRUD._makeRequest - $link',
|
|
||||||
);
|
|
||||||
return 'failure';
|
return 'failure';
|
||||||
}
|
}
|
||||||
|
} on TimeoutException {
|
||||||
|
// معالجة صامتة للتايم أوت في الـ GET
|
||||||
|
return 'failure';
|
||||||
} on SocketException {
|
} on SocketException {
|
||||||
// 2. This block now catches the "no internet" case after all retries have failed.
|
// معالجة صامتة لانقطاع النت
|
||||||
_netGuard.notifyOnce((title, msg) {
|
return 'no_internet';
|
||||||
mySnackeBarError(msg);
|
} catch (e) {
|
||||||
});
|
addError('GET Exception: $e', '', link);
|
||||||
return 'no_internet'; // Return the specific status you were using before.
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
// 3. This is a general catch-all for any other unexpected errors.
|
|
||||||
addError(
|
|
||||||
'HTTP Request Exception: $e',
|
|
||||||
'Stack Trace: $stackTrace',
|
|
||||||
'CRUD._makeRequest - $link',
|
|
||||||
);
|
|
||||||
return 'failure';
|
return 'failure';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs a standard authenticated POST request.
|
||||||
|
/// Automatically handles token renewal.
|
||||||
Future<dynamic> post({
|
Future<dynamic> post({
|
||||||
required String link,
|
required String link,
|
||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
}) async {
|
}) async {
|
||||||
// 1. Check if the token is expired
|
String token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||||
// bool isTokenExpired = JwtDecoder.isExpired(X
|
// if (JwtDecoder.isExpired(token)) {
|
||||||
// .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
|
// await Get.put(LoginController()).getJWT();
|
||||||
// .toString()
|
// token = r(box.read(BoxName.jwt)).toString().split(Env.addd)[0];
|
||||||
// .split(AppInformation.addd)[0]);
|
|
||||||
|
|
||||||
// // 2. If expired, get a new one
|
|
||||||
// if (isTokenExpired) {
|
|
||||||
// await LoginDriverController().getJWT();
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// 3. Prepare the headers with the valid token
|
|
||||||
final headers = {
|
final headers = {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
'Authorization':
|
'Authorization': 'Bearer $token'
|
||||||
'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. Make the request using the centralized helper
|
|
||||||
return await _makeRequest(
|
return await _makeRequest(
|
||||||
link: link,
|
link: link,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
@@ -208,24 +256,23 @@ class CRUD {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an authenticated POST request to the wallet endpoints.
|
/// Performs a standard authenticated GET request (using POST method as per original code).
|
||||||
/// Uses a separate JWT and HMAC for authentication.
|
/// Automatically handles token renewal.
|
||||||
|
|
||||||
|
/// Performs an authenticated POST request to wallet endpoints.
|
||||||
Future<dynamic> postWallet({
|
Future<dynamic> postWallet({
|
||||||
required String link,
|
required String link,
|
||||||
Map<String, dynamic>? payload,
|
Map<String, dynamic>? payload,
|
||||||
}) async {
|
}) async {
|
||||||
// 1. Get the specific JWT and HMAC for the wallet
|
|
||||||
var jwt = await LoginDriverController().getJwtWallet();
|
var jwt = await LoginDriverController().getJwtWallet();
|
||||||
final hmac = box.read(BoxName.hmac);
|
final hmac = box.read(BoxName.hmac);
|
||||||
|
|
||||||
// 2. Prepare the headers
|
|
||||||
final headers = {
|
final headers = {
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
'Authorization': 'Bearer $jwt',
|
'Authorization': 'Bearer $jwt',
|
||||||
'X-HMAC-Auth': hmac.toString(),
|
'X-HMAC-Auth': hmac.toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. Make the request using the centralized helper
|
|
||||||
return await _makeRequest(
|
return await _makeRequest(
|
||||||
link: link,
|
link: link,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
@@ -233,6 +280,55 @@ class CRUD {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an authenticated GET request to wallet endpoints (using POST).
|
||||||
|
Future<dynamic> getWallet({
|
||||||
|
required String link,
|
||||||
|
Map<String, dynamic>? payload,
|
||||||
|
}) async {
|
||||||
|
var s = await LoginDriverController().getJwtWallet();
|
||||||
|
final hmac = box.read(BoxName.hmac);
|
||||||
|
var url = Uri.parse(
|
||||||
|
link,
|
||||||
|
);
|
||||||
|
var response = await http.post(
|
||||||
|
url,
|
||||||
|
body: payload,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
'Authorization': 'Bearer $s',
|
||||||
|
'X-HMAC-Auth': hmac.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
var jsonData = jsonDecode(response.body);
|
||||||
|
if (jsonData['status'] == 'success') {
|
||||||
|
return response.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonData['status'];
|
||||||
|
} else if (response.statusCode == 401) {
|
||||||
|
// Specifically handle 401 Unauthorized
|
||||||
|
var jsonData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (jsonData['error'] == 'Token expired') {
|
||||||
|
// Show snackbar prompting to re-login
|
||||||
|
await Get.put(LoginDriverController()).getJwtWallet();
|
||||||
|
|
||||||
|
return 'token_expired'; // Return a specific value for token expiration
|
||||||
|
} else {
|
||||||
|
// Other 401 errors
|
||||||
|
addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401',
|
||||||
|
url.toString());
|
||||||
|
return 'failure';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addError('Non-200 response code: ${response.statusCode}',
|
||||||
|
'crud().post - Other', url.toString());
|
||||||
|
return 'failure';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<dynamic> postWalletMtn(
|
Future<dynamic> postWalletMtn(
|
||||||
{required String link, Map<String, dynamic>? payload}) async {
|
{required String link, Map<String, dynamic>? payload}) async {
|
||||||
final s = await LoginDriverController().getJwtWallet();
|
final s = await LoginDriverController().getJwtWallet();
|
||||||
@@ -250,11 +346,6 @@ class CRUD {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
print('req: ${response.request}');
|
|
||||||
print('status: ${response.statusCode}');
|
|
||||||
print('body: ${response.body}');
|
|
||||||
print('payload: $payload');
|
|
||||||
|
|
||||||
Map<String, dynamic> wrap(String status, {Object? message, int? code}) {
|
Map<String, dynamic> wrap(String status, {Object? message, int? code}) {
|
||||||
return {
|
return {
|
||||||
'status': status,
|
'status': status,
|
||||||
@@ -305,122 +396,11 @@ class CRUD {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> get({
|
|
||||||
required String link,
|
|
||||||
Map<String, dynamic>? payload,
|
|
||||||
}) async {
|
|
||||||
// bool isTokenExpired = JwtDecoder.isExpired(X
|
|
||||||
// .r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs)
|
|
||||||
// .toString()
|
|
||||||
// .split(AppInformation.addd)[0]);
|
|
||||||
// // Log.print('isTokenExpired: ${isTokenExpired}');
|
|
||||||
|
|
||||||
// if (isTokenExpired) {
|
|
||||||
// await LoginDriverController().getJWT();
|
|
||||||
// }
|
|
||||||
// await Get.put(LoginDriverController()).getJWT();
|
|
||||||
var url = Uri.parse(
|
|
||||||
link,
|
|
||||||
);
|
|
||||||
var response = await http.post(
|
|
||||||
url,
|
|
||||||
body: payload,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
'Authorization':
|
|
||||||
'Bearer ${X.r(X.r(X.r(box.read(BoxName.jwt), cn), cC), cs).toString().split(AppInformation.addd)[0]}'
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// print(response.request);
|
|
||||||
// Log.print('response.body: ${response.body}');
|
|
||||||
// print(payload);
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var jsonData = jsonDecode(response.body);
|
|
||||||
if (jsonData['status'] == 'success') {
|
|
||||||
return response.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonData['status'];
|
|
||||||
} else if (response.statusCode == 401) {
|
|
||||||
// Specifically handle 401 Unauthorized
|
|
||||||
var jsonData = jsonDecode(response.body);
|
|
||||||
|
|
||||||
if (jsonData['error'] == 'Token expired') {
|
|
||||||
// Show snackbar prompting to re-login
|
|
||||||
await Get.put(LoginDriverController()).getJWT();
|
|
||||||
// mySnackbarSuccess('please order now'.tr);
|
|
||||||
|
|
||||||
return 'token_expired'; // Return a specific value for token expiration
|
|
||||||
} else {
|
|
||||||
// Other 401 errors
|
|
||||||
// addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401');
|
|
||||||
return 'failure';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// addError('Non-200 response code: ${response.statusCode}',
|
|
||||||
// 'crud().post - Other');
|
|
||||||
return 'failure';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<dynamic> getWallet({
|
|
||||||
required String link,
|
|
||||||
Map<String, dynamic>? payload,
|
|
||||||
}) async {
|
|
||||||
var s = await LoginDriverController().getJwtWallet();
|
|
||||||
final hmac = box.read(BoxName.hmac);
|
|
||||||
// Log.print('hmac: ${hmac}');
|
|
||||||
var url = Uri.parse(
|
|
||||||
link,
|
|
||||||
);
|
|
||||||
var response = await http.post(
|
|
||||||
url,
|
|
||||||
body: payload,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
'Authorization': 'Bearer $s',
|
|
||||||
'X-HMAC-Auth': hmac.toString(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
// Log.print('response.request: ${response.request}');
|
|
||||||
// Log.print('response.body: ${response.body}');
|
|
||||||
// print(payload);
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
var jsonData = jsonDecode(response.body);
|
|
||||||
if (jsonData['status'] == 'success') {
|
|
||||||
return response.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonData['status'];
|
|
||||||
} else if (response.statusCode == 401) {
|
|
||||||
// Specifically handle 401 Unauthorized
|
|
||||||
var jsonData = jsonDecode(response.body);
|
|
||||||
|
|
||||||
if (jsonData['error'] == 'Token expired') {
|
|
||||||
// Show snackbar prompting to re-login
|
|
||||||
// await Get.put(LoginDriverController()).getJwtWallet();
|
|
||||||
|
|
||||||
return 'token_expired'; // Return a specific value for token expiration
|
|
||||||
} else {
|
|
||||||
// Other 401 errors
|
|
||||||
// addError('Unauthorized: ${jsonData['error']}', 'crud().post - 401');
|
|
||||||
return 'failure';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// addError('Non-200 response code: ${response.statusCode}',
|
|
||||||
// 'crud().post - Other');
|
|
||||||
return 'failure';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Future<dynamic> postWallet(
|
// Future<dynamic> postWallet(
|
||||||
// {required String link, Map<String, dynamic>? payload}) async {
|
// {required String link, Map<String, dynamic>? payload}) async {
|
||||||
// var s = await LoginDriverController().getJwtWallet();
|
// var s = await LoginDriverController().getJwtWallet();
|
||||||
// // Log.print('jwt: ${s}');
|
|
||||||
// final hmac = box.read(BoxName.hmac);
|
// final hmac = box.read(BoxName.hmac);
|
||||||
// // Log.print('hmac: ${hmac}');
|
|
||||||
// var url = Uri.parse(link);
|
// var url = Uri.parse(link);
|
||||||
// // Log.print('url: ${url}');
|
|
||||||
// try {
|
// try {
|
||||||
// // await LoginDriverController().getJWT();
|
// // await LoginDriverController().getJWT();
|
||||||
|
|
||||||
@@ -433,9 +413,6 @@ class CRUD {
|
|||||||
// 'X-HMAC-Auth': hmac.toString(),
|
// 'X-HMAC-Auth': hmac.toString(),
|
||||||
// },
|
// },
|
||||||
// );
|
// );
|
||||||
// // Log.print('response.request:${response.request}');
|
|
||||||
// // Log.print('response.body: ${response.body}');
|
|
||||||
// // Log.print('payload:$payload');
|
|
||||||
// if (response.statusCode == 200) {
|
// if (response.statusCode == 200) {
|
||||||
// try {
|
// try {
|
||||||
// var jsonData = jsonDecode(response.body);
|
// var jsonData = jsonDecode(response.body);
|
||||||
@@ -491,9 +468,6 @@ class CRUD {
|
|||||||
// // 'Authorization': 'Bearer ${box.read(BoxName.jwt)}'
|
// // 'Authorization': 'Bearer ${box.read(BoxName.jwt)}'
|
||||||
// },
|
// },
|
||||||
// );
|
// );
|
||||||
// print(response.request);
|
|
||||||
// Log.print('response.body: ${response.body}');
|
|
||||||
// print(payload);
|
|
||||||
// if (response.statusCode == 200) {
|
// if (response.statusCode == 200) {
|
||||||
// try {
|
// try {
|
||||||
// var jsonData = jsonDecode(response.body);
|
// var jsonData = jsonDecode(response.body);
|
||||||
@@ -763,11 +737,8 @@ class CRUD {
|
|||||||
|
|
||||||
// التحقق من النتيجة
|
// التحقق من النتيجة
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
print("✅ Email sent successfully.");
|
|
||||||
} else {
|
} else {
|
||||||
print("❌ Failed to send email. Status: ${response.statusCode}");
|
|
||||||
final responseBody = await response.stream.bytesToString();
|
final responseBody = await response.stream.bytesToString();
|
||||||
print("Response body: $responseBody");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -867,8 +838,6 @@ class CRUD {
|
|||||||
url,
|
url,
|
||||||
body: payload,
|
body: payload,
|
||||||
);
|
);
|
||||||
Log.print('esponse.body: ${response.body}');
|
|
||||||
Log.print('esponse.body: ${response.request}');
|
|
||||||
var jsonData = jsonDecode(response.body);
|
var jsonData = jsonDecode(response.body);
|
||||||
|
|
||||||
if (jsonData['status'] == 'OK') {
|
if (jsonData['status'] == 'OK') {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import '../../constant/api_key.dart';
|
|||||||
import '../../constant/char_map.dart';
|
import '../../constant/char_map.dart';
|
||||||
import '../../constant/colors.dart';
|
import '../../constant/colors.dart';
|
||||||
import '../../print.dart';
|
import '../../print.dart';
|
||||||
|
import '../firebase/notification_service.dart';
|
||||||
import 'encrypt_decrypt.dart';
|
import 'encrypt_decrypt.dart';
|
||||||
import 'tts.dart';
|
import 'tts.dart';
|
||||||
import 'upload_image.dart';
|
import 'upload_image.dart';
|
||||||
@@ -105,12 +106,20 @@ class AI extends GetxController {
|
|||||||
NotificationController().showNotification(
|
NotificationController().showNotification(
|
||||||
"Code approved".tr, "Code approved".tr, 'tone2', '');
|
"Code approved".tr, "Code approved".tr, 'tone2', '');
|
||||||
// Notification text with dynamic token
|
// Notification text with dynamic token
|
||||||
Get.put(FirebaseMessagesController()).sendNotificationToDriverMAP(
|
// Get.put(FirebaseMessagesController()).sendNotificationToDriverMAP(
|
||||||
'You have received a gift token!'.tr,
|
// 'You have received a gift token!'.tr,
|
||||||
'for '.tr + box.read(BoxName.phoneDriver).toString(),
|
// 'for '.tr + box.read(BoxName.phoneDriver).toString(),
|
||||||
jsonDecode(res)['message'][0]['token'].toString(),
|
// jsonDecode(res)['message'][0]['token'].toString(),
|
||||||
[],
|
// [],
|
||||||
'tone2', // Type of notification
|
// 'tone2', // Type of notification
|
||||||
|
// );
|
||||||
|
NotificationService.sendNotification(
|
||||||
|
target: (res)['message'][0]['token'].toString(),
|
||||||
|
title: 'You have received a gift token!'.tr,
|
||||||
|
body: 'for '.tr + box.read(BoxName.phoneDriver).toString(),
|
||||||
|
isTopic: false, // Important: this is a token
|
||||||
|
tone: 'tone2',
|
||||||
|
driverList: [], category: 'You have received a gift token!',
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// mySnackeBarError(
|
// mySnackeBarError(
|
||||||
|
|||||||
@@ -8,11 +8,39 @@ void showInBrowser(String url) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> makePhoneCall(String phoneNumber) async {
|
Future<void> makePhoneCall(String phoneNumber) async {
|
||||||
|
// 1. Clean the number
|
||||||
|
String formattedNumber = phoneNumber.replaceAll(RegExp(r'\s+'), '');
|
||||||
|
|
||||||
|
// 2. Format logic (Syria/International)
|
||||||
|
if (formattedNumber.length > 6) {
|
||||||
|
if (formattedNumber.startsWith('09')) {
|
||||||
|
formattedNumber = '+963${formattedNumber.substring(1)}';
|
||||||
|
} else if (!formattedNumber.startsWith('+')) {
|
||||||
|
formattedNumber = '+$formattedNumber';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Create URI
|
||||||
final Uri launchUri = Uri(
|
final Uri launchUri = Uri(
|
||||||
scheme: 'tel',
|
scheme: 'tel',
|
||||||
path: phoneNumber,
|
path: 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);
|
await launchUrl(launchUri);
|
||||||
|
} else {
|
||||||
|
print("Cannot launch url: $launchUri");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void launchCommunication(
|
void launchCommunication(
|
||||||
|
|||||||
@@ -1,54 +1,74 @@
|
|||||||
// import 'dart:async';
|
import 'dart:io';
|
||||||
// import 'package:background_location/background_location.dart';
|
|
||||||
// import 'package:get/get.dart';
|
|
||||||
// import 'package:permission_handler/permission_handler.dart';
|
|
||||||
|
|
||||||
// class LocationBackgroundController extends GetxController {
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
// @override
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
// void onInit() {
|
|
||||||
// super.onInit();
|
|
||||||
// requestLocationPermission();
|
|
||||||
// configureBackgroundLocation();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Future<void> requestLocationPermission() async {
|
import 'background_service.dart';
|
||||||
// var status = await Permission.locationAlways.status;
|
|
||||||
// if (!status.isGranted) {
|
|
||||||
// await Permission.locationAlways.request();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Future<void> configureBackgroundLocation() async {
|
Future<void> requestNotificationPermission() async {
|
||||||
// await BackgroundLocation.setAndroidNotification(
|
if (Platform.isAndroid) {
|
||||||
// title: 'Location Tracking Active'.tr,
|
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||||
// message: 'Your location is being tracked in the background.'.tr,
|
if (androidInfo.version.sdkInt >= 33) {
|
||||||
// icon: '@mipmap/launcher_icon',
|
// Android 13+
|
||||||
// );
|
final status = await Permission.notification.request();
|
||||||
|
if (!status.isGranted) {
|
||||||
|
print('⚠️ إذن الإشعارات مرفوض');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// BackgroundLocation.setAndroidConfiguration(3000);
|
// بعد الحصول على الإذن، ابدأ الخدمة
|
||||||
// BackgroundLocation.startLocationService();
|
await BackgroundServiceHelper.startService();
|
||||||
// BackgroundLocation.getLocationUpdates((location) {
|
}
|
||||||
// // Handle location updates here
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// startBackLocation() async {
|
class PermissionsHelper {
|
||||||
// Timer.periodic(const Duration(seconds: 3), (timer) {
|
/// طلب إذن الإشعارات على Android 13+
|
||||||
// getBackgroundLocation();
|
static Future<bool> requestNotificationPermission() async {
|
||||||
// });
|
if (Platform.isAndroid) {
|
||||||
// }
|
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||||
|
|
||||||
// getBackgroundLocation() async {
|
// Android 13+ (API 33+) يحتاج إذن POST_NOTIFICATIONS
|
||||||
// var status = await Permission.locationAlways.status;
|
if (androidInfo.version.sdkInt >= 33) {
|
||||||
// if (status.isGranted) {
|
final status = await Permission.notification.request();
|
||||||
// await BackgroundLocation.startLocationService(
|
|
||||||
// distanceFilter: 20, forceAndroidLocationManager: true);
|
|
||||||
// BackgroundLocation.setAndroidConfiguration(
|
|
||||||
// Duration.microsecondsPerSecond); // Set interval to 5 seconds
|
|
||||||
|
|
||||||
// BackgroundLocation.getLocationUpdates((location1) {});
|
if (status.isDenied) {
|
||||||
// } else {
|
print('⚠️ إذن الإشعارات مرفوض');
|
||||||
// await Permission.locationAlways.request();
|
return false;
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
// }
|
if (status.isPermanentlyDenied) {
|
||||||
|
print('⚠️ إذن الإشعارات مرفوض بشكل دائم - افتح الإعدادات');
|
||||||
|
await openAppSettings();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// طلب جميع الإذونات المطلوبة
|
||||||
|
static Future<bool> requestAllPermissions() async {
|
||||||
|
// إذن الإشعارات أولاً
|
||||||
|
bool notificationGranted = await requestNotificationPermission();
|
||||||
|
if (!notificationGranted) return false;
|
||||||
|
|
||||||
|
// إذن الموقع
|
||||||
|
final locationStatus = await Permission.location.request();
|
||||||
|
if (!locationStatus.isGranted) {
|
||||||
|
print('⚠️ إذن الموقع مرفوض');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// إذن الموقع في الخلفية
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final bgLocationStatus = await Permission.locationAlways.request();
|
||||||
|
if (!bgLocationStatus.isGranted) {
|
||||||
|
print('⚠️ إذن الموقع في الخلفية مرفوض');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,244 +1,634 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart' as geo;
|
||||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
import 'package:location/location.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:sefer_driver/constant/table_names.dart';
|
||||||
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
|
|
||||||
|
|
||||||
import '../../constant/box_name.dart';
|
import '../../constant/box_name.dart';
|
||||||
import '../../constant/links.dart';
|
import '../../constant/links.dart';
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
import '../../print.dart';
|
|
||||||
import '../home/captin/home_captain_controller.dart';
|
import '../home/captin/home_captain_controller.dart';
|
||||||
|
import '../home/captin/map_driver_controller.dart';
|
||||||
import '../home/payment/captain_wallet_controller.dart';
|
import '../home/payment/captain_wallet_controller.dart';
|
||||||
import 'battery_status.dart';
|
import 'background_service.dart';
|
||||||
import 'crud.dart';
|
import 'crud.dart';
|
||||||
import 'encrypt_decrypt.dart';
|
|
||||||
|
|
||||||
class LocationController extends GetxController {
|
class LocationController extends GetxController with WidgetsBindingObserver {
|
||||||
LocationData? _currentLocation;
|
// ===================================================================
|
||||||
late Location location = Location();
|
// ====== Tunables ======
|
||||||
bool isLoading = false;
|
// ===================================================================
|
||||||
late double heading = 0;
|
static const Duration recordIntervalNormal = Duration(seconds: 3);
|
||||||
late double previousTime = 0;
|
static const Duration uploadBatchIntervalNormal = Duration(minutes: 2);
|
||||||
late double latitude;
|
static const Duration recordIntervalPowerSave = Duration(seconds: 10);
|
||||||
late double totalDistance = 0;
|
static const Duration uploadBatchIntervalPowerSave = Duration(minutes: 5);
|
||||||
late double longitude;
|
|
||||||
late DateTime time;
|
|
||||||
late double speed = 0;
|
|
||||||
bool isActive = false;
|
|
||||||
late LatLng myLocation = LatLng(0, 0);
|
|
||||||
String totalPoints = '0';
|
|
||||||
LocationData? get currentLocation => _currentLocation;
|
|
||||||
Timer? _locationTimer;
|
|
||||||
|
|
||||||
LatLng? _lastSavedPosition;
|
static const double lowWalletThreshold = -200;
|
||||||
|
static const int powerSaveTriggerLevel = 20;
|
||||||
|
static const int powerSaveExitLevel = 25;
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// ====== Services & Variables ======
|
||||||
|
// ===================================================================
|
||||||
|
late final Location location = Location();
|
||||||
|
final Battery _battery = Battery();
|
||||||
|
|
||||||
|
IO.Socket? socket;
|
||||||
|
bool isSocketConnected = false;
|
||||||
|
Timer? _socketHeartbeat;
|
||||||
|
|
||||||
|
StreamSubscription<LocationData>? _locSub;
|
||||||
|
StreamSubscription<BatteryState>? _batterySub;
|
||||||
|
|
||||||
|
Timer? _recordTimer;
|
||||||
|
Timer? _uploadBatchTimer;
|
||||||
|
|
||||||
|
late final HomeCaptainController _homeCtrl;
|
||||||
|
late final CaptainWalletController _walletCtrl;
|
||||||
|
|
||||||
|
LatLng myLocation = const LatLng(0, 0);
|
||||||
|
double heading = 0.0;
|
||||||
|
double speed = 0.0;
|
||||||
|
double totalDistance = 0.0;
|
||||||
|
bool _isReady = false;
|
||||||
|
bool _isPowerSavingMode = false;
|
||||||
|
|
||||||
|
final List<Map<String, dynamic>> _trackBuffer = [];
|
||||||
|
|
||||||
|
LatLng? _lastPosForDistance;
|
||||||
|
LatLng? _lastRecordedRealLoc;
|
||||||
|
DateTime? _lastRecordedTime;
|
||||||
|
|
||||||
|
LatLng? _lastSqlLoc;
|
||||||
|
double? _lastSpeed;
|
||||||
|
DateTime? _lastSpeedAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() async {
|
Future<void> onInit() async {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
location = Location();
|
print('🚀 LocationController Starting...');
|
||||||
await location.changeSettings(
|
|
||||||
accuracy: LocationAccuracy.high, interval: 5000, distanceFilter: 0);
|
// 1. Register Lifecycle Observer
|
||||||
location.enableBackgroundMode(enable: true);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
await getLocation();
|
box.write(BoxName.isAppInForeground, true);
|
||||||
|
|
||||||
|
// مراقب الحالة (Status Watcher)
|
||||||
|
box.listenKey(BoxName.statusDriverLocation, (value) {
|
||||||
|
if (value == 'blocked') {
|
||||||
|
print("⛔ Driver is Blocked: Force Stopping Location Updates.");
|
||||||
|
stopLocationUpdates();
|
||||||
|
if (socket != null && socket!.connected) {
|
||||||
|
socket!.emit('update_location', {
|
||||||
|
'driver_id': box.read(BoxName.driverID),
|
||||||
|
'status': 'blocked',
|
||||||
|
'lat': myLocation.latitude,
|
||||||
|
'lng': myLocation.longitude,
|
||||||
|
'heading': heading,
|
||||||
|
'speed': speed * 3.6,
|
||||||
|
'distance': totalDistance
|
||||||
|
});
|
||||||
|
socket!.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bool deps = await _awaitDependencies();
|
||||||
|
if (!deps) return;
|
||||||
|
|
||||||
|
_isReady = true;
|
||||||
|
|
||||||
|
initSocket();
|
||||||
|
await _initLocationSettings();
|
||||||
|
_listenToBatteryChanges();
|
||||||
|
|
||||||
|
if (box.read(BoxName.statusDriverLocation) != 'blocked') {
|
||||||
await startLocationUpdates();
|
await startLocationUpdates();
|
||||||
|
|
||||||
totalPoints = Get.put(CaptainWalletController()).totalPoints.toString();
|
|
||||||
isActive = Get.put(HomeCaptainController()).isActive;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String getLocationArea(double latitude, double longitude) {
|
print('✅ LocationController Initialized.');
|
||||||
final locations = box.read(BoxName.locationName) ?? [];
|
}
|
||||||
for (final location in locations) {
|
|
||||||
final locationData = location as Map<String, dynamic>;
|
|
||||||
final minLatitude =
|
|
||||||
double.tryParse(locationData['min_latitude'].toString()) ?? 0.0;
|
|
||||||
final maxLatitude =
|
|
||||||
double.tryParse(locationData['max_latitude'].toString()) ?? 0.0;
|
|
||||||
final minLongitude =
|
|
||||||
double.tryParse(locationData['min_longitude'].toString()) ?? 0.0;
|
|
||||||
final maxLongitude =
|
|
||||||
double.tryParse(locationData['max_longitude'].toString()) ?? 0.0;
|
|
||||||
|
|
||||||
if (latitude >= minLatitude &&
|
@override
|
||||||
latitude <= maxLatitude &&
|
void onClose() {
|
||||||
longitude >= minLongitude &&
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
longitude <= maxLongitude) {
|
box.write(BoxName.isAppInForeground, false);
|
||||||
box.write(BoxName.serverChosen, (locationData['server_link']));
|
stopLocationUpdates();
|
||||||
return locationData['name'];
|
_batterySub?.cancel();
|
||||||
|
_stopHeartbeat();
|
||||||
|
socket?.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// 🔥 Lifecycle Manager (Fixes Freeze & Background issues)
|
||||||
|
// ===================================================================
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
print("📱 Lifecycle: App is in FOREGROUND");
|
||||||
|
box.write(BoxName.isAppInForeground, true);
|
||||||
|
|
||||||
|
// إيقاف خدمة الخلفية لأننا في الواجهة الآن
|
||||||
|
BackgroundServiceHelper.stopService();
|
||||||
|
|
||||||
|
// التأكد من أن السوكيت متصل، وإذا لا، نعيد الاتصال فوراً
|
||||||
|
if (socket == null || !socket!.connected) {
|
||||||
|
print("🔄 Socket disconnected in background. Reconnecting now...");
|
||||||
|
initSocket();
|
||||||
|
} else {
|
||||||
|
// إذا كان متصلاً، ننعش المستمعين فقط للتأكد
|
||||||
|
_setupSocketListeners();
|
||||||
|
}
|
||||||
|
} else if (state == AppLifecycleState.paused ||
|
||||||
|
state == AppLifecycleState.detached) {
|
||||||
|
print("📱 Lifecycle: App is in BACKGROUND");
|
||||||
|
box.write(BoxName.isAppInForeground, false);
|
||||||
|
|
||||||
|
// تشغيل خدمة الخلفية لضمان بقاء التطبيق حياً
|
||||||
|
BackgroundServiceHelper.startService();
|
||||||
|
|
||||||
|
// ملاحظة: لا نقطع السوكيت هنا، نتركه يعمل قدر الإمكان
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
box.write(BoxName.serverChosen, AppLink.seferCairoServer);
|
Future<bool> _awaitDependencies() async {
|
||||||
return 'Cairo';
|
int attempts = 0;
|
||||||
|
while (attempts < 10) {
|
||||||
|
if (Get.isRegistered<HomeCaptainController>() &&
|
||||||
|
Get.isRegistered<CaptainWalletController>()) {
|
||||||
|
_homeCtrl = Get.find<HomeCaptainController>();
|
||||||
|
_walletCtrl = Get.find<CaptainWalletController>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int _insertCounter = 0;
|
// ===================================================================
|
||||||
double? _lastSpeed;
|
// ====== Socket Logic (Improved) ======
|
||||||
DateTime? _lastSpeedTime;
|
// ===================================================================
|
||||||
|
|
||||||
Future<void> startLocationUpdates() async {
|
void initSocket() {
|
||||||
if (box.read(BoxName.driverID) != null) {
|
String driverId = box.read(BoxName.driverID).toString();
|
||||||
_locationTimer =
|
String token = box.read(BoxName.tokenDriver).toString();
|
||||||
Timer.periodic(const Duration(seconds: 5), (timer) async {
|
String platform = Platform.isIOS ? 'ios' : 'android';
|
||||||
|
|
||||||
|
// 1. إذا كان السوكيت موجوداً، فقط تأكد من اتصاله
|
||||||
|
if (socket != null) {
|
||||||
|
if (!socket!.connected) {
|
||||||
|
print("🟡 Socket exists but disconnected. Reconnecting...");
|
||||||
|
socket!.connect();
|
||||||
|
}
|
||||||
|
_setupSocketListeners(); // تحديث المستمعين
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
print("🟡 [LocationController] Creating 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());
|
||||||
|
|
||||||
|
socket!.connect();
|
||||||
|
_setupSocketListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// دالة منفصلة لضمان عدم تكرار المستمعين
|
||||||
|
void _setupSocketListeners() {
|
||||||
|
if (socket == null) return;
|
||||||
|
|
||||||
|
// تنظيف القديم أولاً
|
||||||
|
socket!.off('connect');
|
||||||
|
socket!.off('disconnect');
|
||||||
|
socket!.off('new_ride_request');
|
||||||
|
socket!.off('ride_cancelled');
|
||||||
|
|
||||||
|
socket!.onConnect((_) {
|
||||||
|
print('✅ Socket Connected! ID: ${socket?.id}');
|
||||||
|
isSocketConnected = true;
|
||||||
|
_startHeartbeat();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket!.onDisconnect((_) {
|
||||||
|
print('❌ Socket Disconnected');
|
||||||
|
isSocketConnected = false;
|
||||||
|
_stopHeartbeat();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔥 الاستماع للطلبات الجديدة
|
||||||
|
socket!.on('new_ride_request', (data) {
|
||||||
|
print("🔔 Socket: New Ride Request Arrived!");
|
||||||
|
|
||||||
|
// نستخدم Future.microtask لضمان عدم حظر الـ UI Thread
|
||||||
|
Future.microtask(() {
|
||||||
|
if (data != null) {
|
||||||
try {
|
try {
|
||||||
await BatteryNotifier.checkBatteryAndNotify();
|
List<dynamic> rawList = [];
|
||||||
totalPoints =
|
if (data is String) {
|
||||||
Get.find<CaptainWalletController>().totalPoints.toString();
|
var decoded = jsonDecode(data);
|
||||||
isActive = Get.find<HomeCaptainController>().isActive;
|
if (decoded is List) rawList = decoded;
|
||||||
|
} else if (data is List) {
|
||||||
if (isActive && double.parse(totalPoints) > -30000) {
|
if (data.isNotEmpty) {
|
||||||
await getLocation();
|
rawList = (data[0] is List) ? data[0] : data;
|
||||||
if (myLocation.latitude == 0 && myLocation.longitude == 0) return;
|
|
||||||
|
|
||||||
String area =
|
|
||||||
getLocationArea(myLocation.latitude, myLocation.longitude);
|
|
||||||
|
|
||||||
final payload = {
|
|
||||||
'driver_id': box.read(BoxName.driverID).toString(),
|
|
||||||
'latitude': myLocation.latitude.toString(),
|
|
||||||
'longitude': myLocation.longitude.toString(),
|
|
||||||
'heading': heading.toString(),
|
|
||||||
'speed': (speed * 3.6).toStringAsFixed(1),
|
|
||||||
'distance': totalDistance.toStringAsFixed(2),
|
|
||||||
'status': box.read(BoxName.statusDriverLocation) ?? 'off',
|
|
||||||
};
|
|
||||||
|
|
||||||
// ✅ تحديث للسيرفر
|
|
||||||
await CRUD().post(
|
|
||||||
link: '${AppLink.server}/ride/location/update.php',
|
|
||||||
payload: payload,
|
|
||||||
);
|
|
||||||
|
|
||||||
// ✅ تخزين في SQLite فقط إذا الرحلة On + تحرك أكثر من 10 متر
|
|
||||||
if ((box.read(BoxName.statusDriverLocation) ?? 'off') == 'on') {
|
|
||||||
if (_lastSavedPosition == null ||
|
|
||||||
_calculateDistanceInMeters(_lastSavedPosition!, myLocation) >=
|
|
||||||
10) {
|
|
||||||
double currentSpeed = speed; // m/s
|
|
||||||
double? acceleration = _calculateAcceleration(currentSpeed);
|
|
||||||
|
|
||||||
await sql.insertData({
|
|
||||||
'driver_id': box.read(BoxName.driverID).toString(),
|
|
||||||
'latitude': myLocation.latitude,
|
|
||||||
'longitude': myLocation.longitude,
|
|
||||||
'acceleration': acceleration ?? 0.0,
|
|
||||||
'created_at': DateTime.now().toIso8601String(),
|
|
||||||
'updated_at': DateTime.now().toIso8601String(),
|
|
||||||
}, TableName.behavior);
|
|
||||||
|
|
||||||
_lastSavedPosition = myLocation;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ إدخال للسيرفر كل دقيقة
|
if (rawList.isNotEmpty) {
|
||||||
_insertCounter++;
|
Map<String, dynamic> convertedMap = {};
|
||||||
// Log.print('_insertCounter: ${_insertCounter}');
|
for (int i = 0; i < rawList.length; i++) {
|
||||||
if (_insertCounter == 12) {
|
convertedMap[i.toString()] = rawList[i];
|
||||||
_insertCounter = 0;
|
|
||||||
await CRUD().post(
|
|
||||||
link: '${AppLink.server}/ride/location/add.php',
|
|
||||||
payload: payload,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
handleIncomingOrder(convertedMap, "Socket");
|
||||||
// ✅ تحديث الكاميرا
|
|
||||||
Get.find<HomeCaptainController>()
|
|
||||||
.mapHomeCaptainController
|
|
||||||
?.animateCamera(
|
|
||||||
CameraUpdate.newCameraPosition(
|
|
||||||
CameraPosition(
|
|
||||||
bearing: Get.find<LocationController>().heading,
|
|
||||||
target: myLocation,
|
|
||||||
zoom: 17, // Adjust zoom level as needed
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// if (Get.isRegistered()) {
|
|
||||||
// Get.find<MapDriverController>().mapController?.animateCamera(
|
|
||||||
// CameraUpdate.newCameraPosition(
|
|
||||||
// CameraPosition(
|
|
||||||
// bearing: Get.find<LocationController>().heading,
|
|
||||||
// target: myLocation,
|
|
||||||
// zoom: 17, // Adjust zoom level as needed
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Location update error: $e');
|
print("❌ Error processing socket data: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔥 الاستماع للإلغاء
|
||||||
|
socket!.on('cancel_ride', (data) {
|
||||||
|
print("🚫 Socket: Ride Cancelled Event Received");
|
||||||
|
String reason = data['reason'] ?? 'No reason provided';
|
||||||
|
if (Get.isRegistered<MapDriverController>()) {
|
||||||
|
Get.find<MapDriverController>()
|
||||||
|
.processRideCancelledByPassenger(reason, source: "Socket");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// داخل LocationController
|
||||||
|
|
||||||
|
Future<void> handleIncomingOrder(
|
||||||
|
Map<String, dynamic> rideData, String source) async {
|
||||||
|
print("📦 Socket Order Received from ($source)");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. التحقق من صحة البيانات
|
||||||
|
if (rideData.isEmpty || !rideData.containsKey('16')) {
|
||||||
|
print("❌ Socket Error: Invalid Ride Data.");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void stopLocationUpdates() {
|
// 2. تجهيز البيانات (DriverList)
|
||||||
_locationTimer?.cancel();
|
List<dynamic> driverList = [];
|
||||||
|
if (rideData.length > 0) {
|
||||||
|
var sortedKeys = rideData.keys.map((e) => int.tryParse(e) ?? 0).toList()
|
||||||
|
..sort();
|
||||||
|
for (var key in sortedKeys) {
|
||||||
|
driverList.add(rideData[key.toString()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/////////
|
||||||
|
// 3. التنقل (باستخدام الاسم لضمان عمل الشرط)
|
||||||
|
try {
|
||||||
|
if (await FlutterOverlayWindow.isActive()) {
|
||||||
|
print("📲 Closing Overlay because App took control via Socket");
|
||||||
|
await FlutterOverlayWindow.closeOverlay();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Overlay check error: $e");
|
||||||
|
}
|
||||||
|
// ✅ هذا الشرط سيعمل الآن بدقة لأننا سنستخدم toNamed
|
||||||
|
if (Get.currentRoute != '/OrderRequestPage') {
|
||||||
|
print("🚀 Socket: Navigating to OrderRequestPage...");
|
||||||
|
|
||||||
|
// 🔥 التعديل هنا: استخدمنا Get.toNamed بدلاً من Get.to
|
||||||
|
// هذا يضمن تطابق الاسم مع ما هو موجود في main.dart
|
||||||
|
Get.toNamed('/OrderRequestPage', arguments: {
|
||||||
|
'myListString': jsonEncode(driverList),
|
||||||
|
'DriverList': driverList,
|
||||||
|
'body': 'New Trip Request via Socket ⚡'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
print("⚠️ User is already on OrderRequestPage. Skipping navigation.");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("❌ Socket Navigation Error: $e");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getLocation() async {
|
void _startHeartbeat() {
|
||||||
|
_socketHeartbeat?.cancel();
|
||||||
|
_socketHeartbeat = Timer.periodic(const Duration(seconds: 25), (timer) {
|
||||||
|
if (socket != null && isSocketConnected && myLocation.latitude != 0) {
|
||||||
|
emitLocationToSocket(myLocation, heading, speed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopHeartbeat() {
|
||||||
|
_socketHeartbeat?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// In LocationController.dart
|
||||||
|
|
||||||
|
void emitLocationToSocket(LatLng pos, double head, double spd) {
|
||||||
|
String status = box.read(BoxName.statusDriverLocation) ?? 'on';
|
||||||
|
|
||||||
|
// Basic payload
|
||||||
|
var payload = {
|
||||||
|
'driver_id': box.read(BoxName.driverID),
|
||||||
|
'lat': pos.latitude,
|
||||||
|
'lng': pos.longitude,
|
||||||
|
'heading': head,
|
||||||
|
'speed': spd * 3.6,
|
||||||
|
'status': status,
|
||||||
|
'distance': totalDistance,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 🔥 CRITICAL FIX: Inject Passenger ID if a ride is active 🔥
|
||||||
|
if (Get.isRegistered<MapDriverController>()) {
|
||||||
|
final mapCtrl = Get.find<MapDriverController>();
|
||||||
|
|
||||||
|
// Check if ride is started/active and we have a passenger ID
|
||||||
|
if (mapCtrl.isRideStarted && mapCtrl.passengerId != null) {
|
||||||
|
payload['passenger_id'] =
|
||||||
|
mapCtrl.passengerId; // This triggers the PHP forwarding
|
||||||
|
payload['ride_id'] = mapCtrl.rideId; // Good for debugging
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug print to verify
|
||||||
|
// print('🚀 Emitting Location: $payload');
|
||||||
|
|
||||||
|
if (socket != null && socket!.connected) {
|
||||||
|
socket!.emit('update_location', payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ===================================================================
|
||||||
|
// ====== Tracking Logic ======
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
Future<void> startLocationUpdates() async {
|
||||||
|
_isReady = true;
|
||||||
|
String currentStatus = box.read(BoxName.statusDriverLocation) ?? 'off';
|
||||||
|
if (currentStatus == 'blocked') {
|
||||||
|
stopLocationUpdates();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start background service
|
||||||
|
await BackgroundServiceHelper.startService();
|
||||||
|
|
||||||
|
if (socket == null || !socket!.connected) {
|
||||||
|
initSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_locSub != null) return;
|
||||||
|
|
||||||
|
if (await _ensureServiceAndPermission()) {
|
||||||
|
_subscribeLocationStream();
|
||||||
|
_startBatchTimers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _subscribeLocationStream() async {
|
||||||
|
_locSub?.cancel();
|
||||||
|
int interval = _isPowerSavingMode ? 10000 : 5000;
|
||||||
|
await location.enableBackgroundMode(enable: true);
|
||||||
|
location.changeSettings(
|
||||||
|
accuracy: LocationAccuracy.navigation,
|
||||||
|
interval: interval,
|
||||||
|
distanceFilter: _isPowerSavingMode ? 20 : 10,
|
||||||
|
);
|
||||||
|
|
||||||
|
_locSub = location.onLocationChanged.listen((LocationData loc) async {
|
||||||
|
if (loc.latitude == null || loc.longitude == null) return;
|
||||||
|
|
||||||
|
final now = DateTime.now();
|
||||||
|
final pos = LatLng(loc.latitude!, loc.longitude!);
|
||||||
|
|
||||||
|
myLocation = pos;
|
||||||
|
speed = loc.speed ?? 0.0;
|
||||||
|
heading = loc.heading ?? 0.0;
|
||||||
|
|
||||||
|
if (_lastPosForDistance != null) {
|
||||||
|
final d = _calculateDistance(_lastPosForDistance!, pos);
|
||||||
|
if (d > 5.0) totalDistance += d;
|
||||||
|
}
|
||||||
|
_lastPosForDistance = pos;
|
||||||
|
|
||||||
|
update();
|
||||||
|
emitLocationToSocket(pos, heading, speed);
|
||||||
|
await _saveBehaviorIfMoved(pos, now, currentSpeed: speed);
|
||||||
|
}, onError: (e) => print('❌ Location Stream Error: $e'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stopLocationUpdates() async {
|
||||||
|
print("🛑 Stopping Location Updates...");
|
||||||
|
|
||||||
|
_locSub?.cancel();
|
||||||
|
_locSub = null;
|
||||||
|
_recordTimer?.cancel();
|
||||||
|
_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();
|
||||||
|
}
|
||||||
|
|
||||||
|
await BackgroundServiceHelper.stopService();
|
||||||
|
socket = null;
|
||||||
|
isSocketConnected = false;
|
||||||
|
_isReady = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// ====== Batch Logic & Helpers ======
|
||||||
|
// ===================================================================
|
||||||
|
|
||||||
|
void _startBatchTimers() {
|
||||||
|
_recordTimer?.cancel();
|
||||||
|
_uploadBatchTimer?.cancel();
|
||||||
|
|
||||||
|
final recDur =
|
||||||
|
_isPowerSavingMode ? recordIntervalPowerSave : recordIntervalNormal;
|
||||||
|
final upDur = _isPowerSavingMode
|
||||||
|
? uploadBatchIntervalPowerSave
|
||||||
|
: uploadBatchIntervalNormal;
|
||||||
|
|
||||||
|
_recordTimer =
|
||||||
|
Timer.periodic(recDur, (_) => _recordCurrentLocationToBuffer());
|
||||||
|
_uploadBatchTimer = Timer.periodic(upDur, (_) => _flushBufferToServer());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _recordCurrentLocationToBuffer() {
|
||||||
|
if (myLocation.latitude == 0) return;
|
||||||
|
final now = DateTime.now();
|
||||||
|
double distFromLast = 0.0;
|
||||||
|
if (_lastRecordedRealLoc != null) {
|
||||||
|
distFromLast = _calculateDistance(_lastRecordedRealLoc!, myLocation);
|
||||||
|
}
|
||||||
|
bool moved = distFromLast > 10.0;
|
||||||
|
bool timeForced = _lastRecordedTime == null ||
|
||||||
|
now.difference(_lastRecordedTime!).inSeconds >= 60;
|
||||||
|
|
||||||
|
if ((moved && speed > 0.5) || timeForced) {
|
||||||
|
_lastRecordedRealLoc = myLocation;
|
||||||
|
_lastRecordedTime = now;
|
||||||
|
final point = {
|
||||||
|
'lat': double.parse(myLocation.latitude.toStringAsFixed(6)),
|
||||||
|
'lng': double.parse(myLocation.longitude.toStringAsFixed(6)),
|
||||||
|
'spd': double.parse((speed * 3.6).toStringAsFixed(1)),
|
||||||
|
'head': int.parse(heading.toStringAsFixed(0)),
|
||||||
|
'st': box.read(BoxName.statusDriverLocation) ?? 'off',
|
||||||
|
'ts': now.toIso8601String(),
|
||||||
|
};
|
||||||
|
_trackBuffer.add(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _flushBufferToServer() async {
|
||||||
|
if (_trackBuffer.isEmpty) return;
|
||||||
|
List<Map<String, dynamic>> batch = List.from(_trackBuffer);
|
||||||
|
_trackBuffer.clear();
|
||||||
|
final String driverId = (box.read(BoxName.driverID) ?? '').toString();
|
||||||
|
try {
|
||||||
|
await CRUD().post(
|
||||||
|
link: '${AppLink.locationServer}/add_batch.php',
|
||||||
|
payload: {'driver_id': driverId, 'batch_data': jsonEncode(batch)},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Failed to upload batch: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listenToBatteryChanges() async {
|
||||||
|
_battery.onBatteryStateChanged.listen((state) async {
|
||||||
|
int level = await _battery.batteryLevel;
|
||||||
|
bool previousMode = _isPowerSavingMode;
|
||||||
|
if (level <= powerSaveTriggerLevel) _isPowerSavingMode = true;
|
||||||
|
if (level >= powerSaveExitLevel) _isPowerSavingMode = false;
|
||||||
|
if (previousMode != _isPowerSavingMode) {
|
||||||
|
_startBatchTimers();
|
||||||
|
startLocationUpdates();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
print('SQLite Error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// استبدال دالة Haversine اليدوية بـ Geolocator في باقي الكود أيضاً
|
||||||
|
// لأنها تعتمد على C++ في الأندرويد و Obj-C في الآيفون (Native Speed)
|
||||||
|
double _calculateDistance(LatLng a, LatLng b) {
|
||||||
|
return geo.Geolocator.distanceBetween(
|
||||||
|
a.latitude, a.longitude, b.latitude, b.longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
double? _calcAcceleration(double currentSpeed, DateTime now) {
|
||||||
|
if (_lastSpeed != null && _lastSpeedAt != null) {
|
||||||
|
final dt = now.difference(_lastSpeedAt!).inMilliseconds / 1000.0;
|
||||||
|
if (dt > 0.5) {
|
||||||
|
final a = (currentSpeed - _lastSpeed!) / dt;
|
||||||
|
_lastSpeed = currentSpeed;
|
||||||
|
_lastSpeedAt = now;
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_lastSpeed = currentSpeed;
|
||||||
|
_lastSpeedAt = now;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initLocationSettings() async {
|
||||||
|
if (await _ensureServiceAndPermission()) {
|
||||||
|
try {
|
||||||
|
await location.enableBackgroundMode(enable: true);
|
||||||
|
location.changeSettings(
|
||||||
|
accuracy: LocationAccuracy.navigation,
|
||||||
|
interval: 1000,
|
||||||
|
distanceFilter: 10);
|
||||||
|
} catch (e) {
|
||||||
|
print("Warning: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥🔥 هذه هي الدالة المعدلة التي تستخدم ph.Permission 🔥🔥
|
||||||
|
Future<bool> _ensureServiceAndPermission() async {
|
||||||
|
// 1. طلب إذن الإشعارات أولاً باستخدام permission_handler
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
var notificationStatus = await ph.Permission.notification.status;
|
||||||
|
if (!notificationStatus.isGranted) {
|
||||||
|
await ph.Permission.notification.request();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. طلب تفعيل خدمة الموقع (GPS) من بكج location
|
||||||
bool serviceEnabled = await location.serviceEnabled();
|
bool serviceEnabled = await location.serviceEnabled();
|
||||||
if (!serviceEnabled) {
|
if (!serviceEnabled) {
|
||||||
serviceEnabled = await location.requestService();
|
serviceEnabled = await location.requestService();
|
||||||
if (!serviceEnabled) return;
|
if (!serviceEnabled) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. طلب إذن الموقع الأساسي من بكج location
|
||||||
PermissionStatus permissionGranted = await location.hasPermission();
|
PermissionStatus permissionGranted = await location.hasPermission();
|
||||||
if (permissionGranted == PermissionStatus.denied) {
|
if (permissionGranted == PermissionStatus.denied) {
|
||||||
permissionGranted = await location.requestPermission();
|
permissionGranted = await location.requestPermission();
|
||||||
if (permissionGranted != PermissionStatus.granted) return;
|
if (permissionGranted != PermissionStatus.granted) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future.delayed(Duration(milliseconds: 500), () async {
|
return true;
|
||||||
|
}
|
||||||
|
// ... (باقي الكود)
|
||||||
|
|
||||||
|
Future<LocationData?> getLocation() async {
|
||||||
try {
|
try {
|
||||||
LocationData _locationData = await location.getLocation();
|
if (await _ensureServiceAndPermission()) {
|
||||||
if (_locationData.latitude != null && _locationData.longitude != null) {
|
return await location.getLocation();
|
||||||
myLocation =
|
|
||||||
LatLng(_locationData.latitude!, _locationData.longitude!);
|
|
||||||
} else {
|
|
||||||
myLocation = LatLng(0, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
speed = _locationData.speed ?? 0;
|
|
||||||
heading = _locationData.heading ?? 0;
|
|
||||||
|
|
||||||
update();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Error getting location: $e");
|
print('❌ FAILED to get single location: $e');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
double _calculateDistanceInMeters(LatLng start, LatLng end) {
|
|
||||||
const p = 0.017453292519943295;
|
|
||||||
final a = 0.5 -
|
|
||||||
cos((end.latitude - start.latitude) * p) / 2 +
|
|
||||||
cos(start.latitude * p) *
|
|
||||||
cos(end.latitude * p) *
|
|
||||||
(1 - cos((end.longitude - start.longitude) * p)) /
|
|
||||||
2;
|
|
||||||
return 12742 * 1000 * asin(sqrt(a)); // meters
|
|
||||||
}
|
|
||||||
|
|
||||||
double? _calculateAcceleration(double currentSpeed) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
if (_lastSpeed != null && _lastSpeedTime != null) {
|
|
||||||
final deltaTime =
|
|
||||||
now.difference(_lastSpeedTime!).inMilliseconds / 1000.0; // seconds
|
|
||||||
if (deltaTime > 0) {
|
|
||||||
final acceleration = (currentSpeed - _lastSpeed!) / deltaTime;
|
|
||||||
_lastSpeed = currentSpeed;
|
|
||||||
_lastSpeedTime = now;
|
|
||||||
return double.parse(acceleration.toStringAsFixed(2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastSpeed = currentSpeed;
|
|
||||||
_lastSpeedTime = now;
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:sefer_driver/views/home/on_boarding_page.dart';
|
import 'package:sefer_driver/views/home/on_boarding_page.dart';
|
||||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -9,12 +7,10 @@ import 'package:sefer_driver/constant/colors.dart';
|
|||||||
import 'package:sefer_driver/constant/links.dart';
|
import 'package:sefer_driver/constant/links.dart';
|
||||||
import 'package:sefer_driver/controller/functions/crud.dart';
|
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||||
import 'package:sefer_driver/main.dart';
|
import 'package:sefer_driver/main.dart';
|
||||||
import 'package:sefer_driver/onbording_page.dart';
|
|
||||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||||
import 'package:sefer_driver/views/widgets/my_textField.dart';
|
import 'package:sefer_driver/views/widgets/my_textField.dart';
|
||||||
|
|
||||||
import '../../constant/style.dart';
|
import '../../constant/style.dart';
|
||||||
import 'encrypt_decrypt.dart';
|
|
||||||
|
|
||||||
class LogOutController extends GetxController {
|
class LogOutController extends GetxController {
|
||||||
TextEditingController checkTxtController = TextEditingController();
|
TextEditingController checkTxtController = TextEditingController();
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ Future<void> showDriverGiftClaim(BuildContext context) async {
|
|||||||
box.read(BoxName.is_claimed) == null) {
|
box.read(BoxName.is_claimed) == null) {
|
||||||
MyDialog().getDialog(
|
MyDialog().getDialog(
|
||||||
'You have gift 30000 SYP'.tr, 'This for new registration'.tr, () async {
|
'You have gift 30000 SYP'.tr, 'This for new registration'.tr, () async {
|
||||||
|
Get.back();
|
||||||
var res = await CRUD().post(link: AppLink.updateDriverClaim, payload: {
|
var res = await CRUD().post(link: AppLink.updateDriverClaim, payload: {
|
||||||
'driverId': box.read(BoxName.driverID),
|
'driverId': box.read(BoxName.driverID),
|
||||||
});
|
});
|
||||||
@@ -49,7 +50,6 @@ Future<void> showDriverGiftClaim(BuildContext context) async {
|
|||||||
);
|
);
|
||||||
box.write(BoxName.is_claimed, '1');
|
box.write(BoxName.is_claimed, '1');
|
||||||
}
|
}
|
||||||
Get.back();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ Future<String> getPackageInfo() async {
|
|||||||
void showUpdateDialog(BuildContext context) {
|
void showUpdateDialog(BuildContext context) {
|
||||||
final String storeUrl = Platform.isAndroid
|
final String storeUrl = Platform.isAndroid
|
||||||
? 'https://play.google.com/store/apps/details?id=com.intaleq_driver'
|
? 'https://play.google.com/store/apps/details?id=com.intaleq_driver'
|
||||||
: 'https://apps.apple.com/ae/app/intaleq-driver/id6502189302';
|
: 'https://apps.apple.com/jo/app/intaleq-driver/id6482995159';
|
||||||
|
|
||||||
showGeneralDialog(
|
showGeneralDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -187,7 +187,7 @@ class DeviceHelper {
|
|||||||
|
|
||||||
// Extract relevant device information
|
// Extract relevant device information
|
||||||
final String deviceId = Platform.isAndroid
|
final String deviceId = Platform.isAndroid
|
||||||
? deviceData['androidId'] ?? deviceData['serialNumber'] ?? 'unknown'
|
? deviceData['androidId'] ?? deviceData['fingerprint'] ?? 'unknown'
|
||||||
: deviceData['identifierForVendor'] ?? 'unknown';
|
: deviceData['identifierForVendor'] ?? 'unknown';
|
||||||
|
|
||||||
final String deviceModel = deviceData['model'] ?? 'unknown';
|
final String deviceModel = deviceData['model'] ?? 'unknown';
|
||||||
@@ -199,6 +199,7 @@ class DeviceHelper {
|
|||||||
|
|
||||||
// Generate and return the encrypted fingerprint
|
// Generate and return the encrypted fingerprint
|
||||||
final String fingerprint = '${deviceId}_${deviceModel}_$osVersion';
|
final String fingerprint = '${deviceId}_${deviceModel}_$osVersion';
|
||||||
|
// Log.print('fingerprint: ${fingerprint}');
|
||||||
// print(EncryptionHelper.instance.encryptData(fingerprint));
|
// print(EncryptionHelper.instance.encryptData(fingerprint));
|
||||||
return (fingerprint);
|
return (fingerprint);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,52 +1,93 @@
|
|||||||
import 'package:sefer_driver/constant/box_name.dart';
|
import 'dart:io';
|
||||||
import 'package:sefer_driver/main.dart';
|
|
||||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_tts/flutter_tts.dart';
|
import 'package:flutter_tts/flutter_tts.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sefer_driver/constant/box_name.dart';
|
||||||
|
import 'package:sefer_driver/main.dart';
|
||||||
|
|
||||||
class TextToSpeechController extends GetxController {
|
class TextToSpeechController extends GetxController {
|
||||||
final flutterTts = FlutterTts();
|
final FlutterTts flutterTts = FlutterTts();
|
||||||
bool isComplete = false;
|
bool isSpeaking = false;
|
||||||
// Initialize TTS in initState
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
initTts();
|
initTts();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispose of TTS when controller is closed
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
|
flutterTts.stop();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
flutterTts.completionHandler;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to initialize TTS engine
|
// --- 1. تهيئة المحرك بإعدادات قوية للملاحة ---
|
||||||
Future<void> initTts() async {
|
Future<void> initTts() async {
|
||||||
String? lang =
|
try {
|
||||||
WidgetsBinding.instance.platformDispatcher.locale.countryCode;
|
// جلب اللغة المحفوظة أو استخدام العربية كافتراضي
|
||||||
|
String lang = box.read(BoxName.lang) ?? 'ar-SA';
|
||||||
|
|
||||||
|
// تصحيح صيغة اللغة إذا لزم الأمر
|
||||||
|
if (lang == 'ar') lang = 'ar-SA';
|
||||||
|
if (lang == 'en') lang = 'en-US';
|
||||||
|
|
||||||
|
await flutterTts.setLanguage(lang);
|
||||||
|
await flutterTts.setSpeechRate(0.5); // سرعة متوسطة وواضحة
|
||||||
|
await flutterTts.setVolume(1.0);
|
||||||
|
await flutterTts.setPitch(1.0);
|
||||||
|
|
||||||
|
// إعدادات خاصة لضمان عمل الصوت مع الملاحة (خاصة للآيفون)
|
||||||
|
if (Platform.isIOS) {
|
||||||
await flutterTts
|
await flutterTts
|
||||||
.setLanguage(box.read(BoxName.lang).toString()); //'en-US' Set language
|
.setIosAudioCategory(IosTextToSpeechAudioCategory.playback, [
|
||||||
// await flutterTts.setLanguage('ar-SA'); //'en-US' Set language
|
IosTextToSpeechAudioCategoryOptions.mixWithOthers,
|
||||||
// await flutterTts.setLanguage(lang!); //'en-US' Set language
|
IosTextToSpeechAudioCategoryOptions.duckOthers
|
||||||
await flutterTts.setSpeechRate(0.5); // Adjust speech rate
|
]);
|
||||||
await flutterTts.setVolume(1.0); // Set volume
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to speak the given text
|
// الاستماع لحالة الانتهاء
|
||||||
|
flutterTts.setCompletionHandler(() {
|
||||||
|
isSpeaking = false;
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
|
||||||
|
flutterTts.setStartHandler(() {
|
||||||
|
isSpeaking = true;
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print("TTS Init Error: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. دالة التحدث (تقاطع الكلام القديم) ---
|
||||||
Future<void> speakText(String text) async {
|
Future<void> speakText(String text) async {
|
||||||
|
if (text.isEmpty) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await flutterTts.awaitSpeakCompletion(true);
|
// إيقاف أي كلام حالي لضمان نطق التوجيه الجديد فوراً (أهم للملاحة)
|
||||||
|
await flutterTts.stop();
|
||||||
|
|
||||||
var result = await flutterTts.speak(text);
|
var result = await flutterTts.speak(text);
|
||||||
if (result == 1) {
|
if (result == 1) {
|
||||||
// TTS operation has started
|
isSpeaking = true;
|
||||||
// You can perform additional operations here, if needed
|
|
||||||
isComplete = true;
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
mySnackeBarError('Failed to speak text: $error');
|
// لا تعرض سناك بار هنا لتجنب إزعاج السائق أثناء القيادة
|
||||||
|
print('Failed to speak text: $error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. دالة الإيقاف (ضرورية لزر الكتم) ---
|
||||||
|
Future<void> stop() async {
|
||||||
|
try {
|
||||||
|
var result = await flutterTts.stop();
|
||||||
|
if (result == 1) {
|
||||||
|
isSpeaking = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Error stopping TTS: $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,31 +52,58 @@ class DriverBehaviorController extends GetxController {
|
|||||||
double totalSpeed = 0;
|
double totalSpeed = 0;
|
||||||
int hardBrakes = 0;
|
int hardBrakes = 0;
|
||||||
double totalDistance = 0;
|
double totalDistance = 0;
|
||||||
|
|
||||||
|
// متغيرات للمقارنة مع النقطة السابقة
|
||||||
double? prevLat, prevLng;
|
double? prevLat, prevLng;
|
||||||
|
DateTime? prevTime;
|
||||||
|
|
||||||
|
// ترتيب البيانات حسب الوقت لضمان دقة الحساب (اختياري لكن مفضل)
|
||||||
|
// data.sort((a, b) => a['created_at'].compareTo(b['created_at']));
|
||||||
|
|
||||||
for (var item in data) {
|
for (var item in data) {
|
||||||
double speed = item['speed'] ?? 0;
|
// 1. قراءة البيانات بالأسماء الصحيحة من الجدول
|
||||||
double lat = item['lat'] ?? 0;
|
double lat = item['latitude'] ?? item['lat'] ?? 0.0;
|
||||||
double lng = item['lng'] ?? 0;
|
double lng = item['longitude'] ?? item['lng'] ?? 0.0;
|
||||||
double acc = item['acceleration'] ?? 0;
|
double acc = item['acceleration'] ?? 0.0;
|
||||||
|
|
||||||
if (speed > maxSpeed) maxSpeed = speed;
|
// قراءة الوقت لحساب السرعة
|
||||||
totalSpeed += speed;
|
DateTime currentTime =
|
||||||
|
DateTime.tryParse(item['created_at'].toString()) ?? DateTime.now();
|
||||||
|
|
||||||
// ✅ Hard brake threshold
|
double currentSpeed = 0;
|
||||||
|
|
||||||
|
// 2. حساب السرعة والمسافة إذا وجدت نقطة سابقة
|
||||||
|
if (prevLat != null && prevLng != null && prevTime != null) {
|
||||||
|
double distKm = _calculateDistance(prevLat, prevLng, lat, lng);
|
||||||
|
int timeDiffSeconds = currentTime.difference(prevTime).inSeconds;
|
||||||
|
|
||||||
|
if (timeDiffSeconds > 0) {
|
||||||
|
// السرعة (كم/س) = (المسافة بالكيلومتر * 3600) / الزمن بالثواني
|
||||||
|
currentSpeed = (distKm * 3600) / timeDiffSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalDistance += distKm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// تحديث القيم الإحصائية
|
||||||
|
if (currentSpeed > maxSpeed) maxSpeed = currentSpeed;
|
||||||
|
totalSpeed += currentSpeed;
|
||||||
|
|
||||||
|
// حساب الفرملة القوية (يعتمد على التسارع المحفوظ مسبقاً)
|
||||||
if (acc.abs() > 3.0) hardBrakes++;
|
if (acc.abs() > 3.0) hardBrakes++;
|
||||||
|
|
||||||
// ✅ Distance between points
|
// حفظ النقطة الحالية لتكون هي "السابقة" في الدورة التالية
|
||||||
if (prevLat != null && prevLng != null) {
|
|
||||||
totalDistance += _calculateDistance(prevLat, prevLng, lat, lng);
|
|
||||||
}
|
|
||||||
prevLat = lat;
|
prevLat = lat;
|
||||||
prevLng = lng;
|
prevLng = lng;
|
||||||
|
prevTime = currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
double avgSpeed = totalSpeed / data.length;
|
// تجنب القسمة على صفر
|
||||||
|
double avgSpeed = (data.length > 1) ? totalSpeed / (data.length - 1) : 0;
|
||||||
|
|
||||||
|
// حساب تقييم السلوك
|
||||||
double behaviorScore = 100 - (hardBrakes * 5) - ((maxSpeed > 100) ? 10 : 0);
|
double behaviorScore = 100 - (hardBrakes * 5) - ((maxSpeed > 100) ? 10 : 0);
|
||||||
behaviorScore = behaviorScore.clamp(0, 100);
|
behaviorScore = behaviorScore.clamp(0.0, 100.0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'max_speed': maxSpeed,
|
'max_speed': maxSpeed,
|
||||||
@@ -91,14 +118,16 @@ class DriverBehaviorController extends GetxController {
|
|||||||
final summary = await analyzeData();
|
final summary = await analyzeData();
|
||||||
if (summary.isEmpty) return;
|
if (summary.isEmpty) return;
|
||||||
|
|
||||||
final body = {
|
final Map<String, dynamic> body = {
|
||||||
'driver_id': driverId,
|
'driver_id': driverId,
|
||||||
'trip_id': tripId,
|
'trip_id': tripId,
|
||||||
...summary,
|
...summary, // فيه doubles
|
||||||
};
|
};
|
||||||
|
|
||||||
CRUD().post(link: AppLink.saveBehavior, payload: (body));
|
// اجبر كل القيم على String
|
||||||
|
final payload = body.map((k, v) => MapEntry(k, v?.toString() ?? ''));
|
||||||
|
|
||||||
|
await CRUD().post(link: AppLink.saveBehavior, payload: payload);
|
||||||
await clearData();
|
await clearData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import '../../../constant/colors.dart';
|
|||||||
import '../../functions/launch.dart';
|
import '../../functions/launch.dart';
|
||||||
|
|
||||||
class ContactUsController extends GetxController {
|
class ContactUsController extends GetxController {
|
||||||
final String phone1 = '+201018805430';
|
final String phone1 = '+963992952235';
|
||||||
final String phone2 = '+201080182934';
|
final String phone2 = '+963992952235';
|
||||||
final TimeOfDay workStartTime = const TimeOfDay(hour: 12, minute: 0);
|
final TimeOfDay workStartTime = const TimeOfDay(hour: 12, minute: 0);
|
||||||
final TimeOfDay workEndTime = const TimeOfDay(hour: 19, minute: 0);
|
final TimeOfDay workEndTime = const TimeOfDay(hour: 19, minute: 0);
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ class ContactUsController extends GetxController {
|
|||||||
if (!_isWithinWorkTime(now))
|
if (!_isWithinWorkTime(now))
|
||||||
CupertinoActionSheetAction(
|
CupertinoActionSheetAction(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Work time is from 12:00 - 19:00.\nYou can send a WhatsApp message or email.'
|
'Work time is from 10:00 - 17:00.\nYou can send a WhatsApp message or email.'
|
||||||
.tr),
|
.tr),
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import 'dart:convert';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'dart:ui' as ui; // للألوان
|
||||||
|
// import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
// import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
// import 'package:latlong2/latlong.dart'
|
||||||
|
// as latlng; // هذا مهم جداً للتعامل مع إحداثيات OSM
|
||||||
import 'package:sefer_driver/constant/box_name.dart';
|
import 'package:sefer_driver/constant/box_name.dart';
|
||||||
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
|
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
@@ -16,6 +22,7 @@ import '../../../print.dart';
|
|||||||
import '../../../views/home/my_wallet/walet_captain.dart';
|
import '../../../views/home/my_wallet/walet_captain.dart';
|
||||||
import '../../../views/widgets/elevated_btn.dart';
|
import '../../../views/widgets/elevated_btn.dart';
|
||||||
import '../../firebase/firbase_messge.dart';
|
import '../../firebase/firbase_messge.dart';
|
||||||
|
import '../../functions/background_service.dart';
|
||||||
import '../../functions/crud.dart';
|
import '../../functions/crud.dart';
|
||||||
import '../../functions/location_background_controller.dart';
|
import '../../functions/location_background_controller.dart';
|
||||||
import '../../functions/location_controller.dart';
|
import '../../functions/location_controller.dart';
|
||||||
@@ -27,6 +34,7 @@ class HomeCaptainController extends GetxController {
|
|||||||
Duration activeDuration = Duration.zero;
|
Duration activeDuration = Duration.zero;
|
||||||
Timer? activeTimer;
|
Timer? activeTimer;
|
||||||
Map data = {};
|
Map data = {};
|
||||||
|
bool isHomeMapActive = true;
|
||||||
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
|
BitmapDescriptor carIcon = BitmapDescriptor.defaultMarker;
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
late double kazan = 0;
|
late double kazan = 0;
|
||||||
@@ -47,14 +55,21 @@ class HomeCaptainController extends GetxController {
|
|||||||
String totalMoneyInSEFER = '0';
|
String totalMoneyInSEFER = '0';
|
||||||
String totalDurationToday = '0';
|
String totalDurationToday = '0';
|
||||||
Timer? timer;
|
Timer? timer;
|
||||||
late LatLng myLocation = const LatLng(32, 36);
|
late LatLng myLocation = const LatLng(33.5138, 36.2765);
|
||||||
String totalPoints = '0';
|
String totalPoints = '0';
|
||||||
String countRefuse = '0';
|
String countRefuse = '0';
|
||||||
bool mapType = false;
|
bool mapType = false;
|
||||||
bool mapTrafficON = false;
|
bool mapTrafficON = false;
|
||||||
double widthMapTypeAndTraffic = 50;
|
double widthMapTypeAndTraffic = 50;
|
||||||
|
// === متغيرات الهيت ماب الجديدة ===
|
||||||
|
bool isHeatmapVisible = false;
|
||||||
|
Set<Polygon> heatmapPolygons =
|
||||||
|
{}; // سنستخدم Polygon لرسم المربعات على جوجل مابس
|
||||||
|
|
||||||
// Inject the LocationController class
|
// Inject the LocationController class
|
||||||
final locationController = Get.put(LocationController());
|
// final locationController = Get.put(LocationController());
|
||||||
|
// الكود الصحيح
|
||||||
|
final locationController = Get.find<LocationController>();
|
||||||
// final locationBackController = Get.put(LocationBackgroundController());
|
// final locationBackController = Get.put(LocationBackgroundController());
|
||||||
String formatDuration(Duration duration) {
|
String formatDuration(Duration duration) {
|
||||||
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||||
@@ -63,6 +78,101 @@ class HomeCaptainController extends GetxController {
|
|||||||
return "${duration.inHours}:$twoDigitMinutes:$twoDigitSeconds";
|
return "${duration.inHours}:$twoDigitMinutes:$twoDigitSeconds";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// دالة لتغيير حالة الهيت ماب (عرض/إخفاء)
|
||||||
|
void toggleHeatmap() async {
|
||||||
|
isHeatmapVisible = !isHeatmapVisible;
|
||||||
|
if (isHeatmapVisible) {
|
||||||
|
await fetchAndDrawHeatmap();
|
||||||
|
} else {
|
||||||
|
heatmapPolygons.clear();
|
||||||
|
}
|
||||||
|
update(); // تحديث الواجهة
|
||||||
|
}
|
||||||
|
|
||||||
|
// داخل MapDriverController
|
||||||
|
|
||||||
|
// متغير لتخزين المربعات
|
||||||
|
// Set<Polygon> heatmapPolygons = {};
|
||||||
|
|
||||||
|
// دالة جلب البيانات ورسم الخريطة
|
||||||
|
Future<void> fetchAndDrawHeatmap() async {
|
||||||
|
// استخدم الرابط المباشر لملف JSON لسرعة قصوى
|
||||||
|
final String jsonUrl =
|
||||||
|
"https://api.intaleq.xyz/intaleq/ride/rides/heatmap_live.json";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// نستخدم timestamp لمنع الكاش من الموبايل نفسه
|
||||||
|
final response = await http.get(
|
||||||
|
Uri.parse("$jsonUrl?t=${DateTime.now().millisecondsSinceEpoch}"));
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final List<dynamic> data = json.decode(response.body);
|
||||||
|
_generatePolygons(data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Heatmap Error: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _generatePolygons(List<dynamic> data) {
|
||||||
|
Set<Polygon> tempPolygons = {};
|
||||||
|
|
||||||
|
// الأوفست لرسم المربع (نصف حجم الشبكة)
|
||||||
|
// الشبكة دقتها 0.01 درجة، لذا نصفها 0.005
|
||||||
|
double offset = 0.005;
|
||||||
|
|
||||||
|
for (var point in data) {
|
||||||
|
double lat = double.parse(point['lat'].toString());
|
||||||
|
double lng = double.parse(point['lng'].toString());
|
||||||
|
|
||||||
|
String intensity = point['intensity'] ?? 'low';
|
||||||
|
int count = int.parse(point['count'].toString()); // ✅ جلب العدد
|
||||||
|
|
||||||
|
Color color;
|
||||||
|
Color strokeColor;
|
||||||
|
|
||||||
|
// 🧠 منطق الألوان: ندمج الذكاء (Intensity) مع العدد (Count)
|
||||||
|
if (intensity == 'high' || count >= 5) {
|
||||||
|
// منطقة مشتعلة (أحمر)
|
||||||
|
// إما فيها طلبات ضائعة (Timeout) أو فيها عدد كبير من الطلبات
|
||||||
|
color = Colors.red.withOpacity(0.35);
|
||||||
|
strokeColor = Colors.red.withOpacity(0.8);
|
||||||
|
} else if (intensity == 'medium' || count >= 3) {
|
||||||
|
// منطقة متوسطة (برتقالي)
|
||||||
|
color = Colors.orange.withOpacity(0.35);
|
||||||
|
strokeColor = Colors.orange.withOpacity(0.8);
|
||||||
|
} else {
|
||||||
|
// منطقة خفيفة (أصفر)
|
||||||
|
color = Colors.yellow.withOpacity(0.3);
|
||||||
|
strokeColor = Colors.yellow.withOpacity(0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// رسم المربع
|
||||||
|
tempPolygons.add(Polygon(
|
||||||
|
polygonId: PolygonId("$lat-$lng"),
|
||||||
|
consumeTapEvents: true, // للسماح بالضغط عليه مستقبلاً
|
||||||
|
points: [
|
||||||
|
LatLng(lat - offset, lng - offset),
|
||||||
|
LatLng(lat + offset, lng - offset),
|
||||||
|
LatLng(lat + offset, lng + offset),
|
||||||
|
LatLng(lat - offset, lng + offset),
|
||||||
|
],
|
||||||
|
fillColor: color,
|
||||||
|
strokeColor: strokeColor,
|
||||||
|
strokeWidth: 2,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
heatmapPolygons = tempPolygons;
|
||||||
|
update(); // تحديث الخريطة
|
||||||
|
}
|
||||||
|
|
||||||
|
// دالة لتشغيل الخريطة الحرارية كل فترة (مثلاً عند فتح الصفحة)
|
||||||
|
void startHeatmapCycle() {
|
||||||
|
fetchAndDrawHeatmap();
|
||||||
|
// يمكن تفعيل Timer هنا لو أردت تحديثها تلقائياً كل 5 دقائق
|
||||||
|
}
|
||||||
|
|
||||||
void goToWalletFromConnect() {
|
void goToWalletFromConnect() {
|
||||||
Get.back();
|
Get.back();
|
||||||
Get.back();
|
Get.back();
|
||||||
@@ -89,11 +199,14 @@ class HomeCaptainController extends GetxController {
|
|||||||
|
|
||||||
String stringActiveDuration = '';
|
String stringActiveDuration = '';
|
||||||
void onButtonSelected() {
|
void onButtonSelected() {
|
||||||
// totalPoints = Get.find<CaptainWalletController>().totalPoints;
|
// تم الإصلاح: التأكد من أن المتحكم موجود قبل استخدامه لتجنب الكراش
|
||||||
|
if (!Get.isRegistered<CaptainWalletController>()) {
|
||||||
|
Get.put(CaptainWalletController());
|
||||||
|
}
|
||||||
|
totalPoints = Get.find<CaptainWalletController>().totalPoints;
|
||||||
isActive = !isActive;
|
isActive = !isActive;
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
if (double.parse(totalPoints) > -300) {
|
if (double.parse(totalPoints) > -200) {
|
||||||
locationController.startLocationUpdates();
|
locationController.startLocationUpdates();
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
// locationBackController.startBackLocation();
|
// locationBackController.startBackLocation();
|
||||||
@@ -124,6 +237,107 @@ class HomeCaptainController extends GetxController {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// متغيرات العداد للحظر
|
||||||
|
RxString remainingBlockTimeStr = "".obs;
|
||||||
|
Timer? _blockTimer;
|
||||||
|
|
||||||
|
/// دالة الفحص والدايلوج
|
||||||
|
void checkAndShowBlockDialog() {
|
||||||
|
String? blockStr = box.read(BoxName.blockUntilDate);
|
||||||
|
if (blockStr == null || blockStr.isEmpty) return;
|
||||||
|
|
||||||
|
DateTime blockExpiry = DateTime.parse(blockStr);
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
|
||||||
|
if (now.isBefore(blockExpiry)) {
|
||||||
|
// 1. إجبار السائق على وضع الأوفلاين
|
||||||
|
box.write(BoxName.statusDriverLocation, 'blocked');
|
||||||
|
update();
|
||||||
|
|
||||||
|
// 2. بدء العداد
|
||||||
|
_startBlockCountdown(blockExpiry);
|
||||||
|
|
||||||
|
// 3. إظهار الديالوج المانع
|
||||||
|
Get.defaultDialog(
|
||||||
|
title: "حسابك مقيد مؤقتاً ⛔",
|
||||||
|
titleStyle:
|
||||||
|
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||||
|
barrierDismissible: false, // 🚫 ممنوع الإغلاق بالضغط خارجاً
|
||||||
|
onWillPop: () async => false, // 🚫 ممنوع زر الرجوع في الأندرويد
|
||||||
|
content: Obx(() => Column(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.timer_off_outlined,
|
||||||
|
size: 50, color: Colors.orange),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
const Text(
|
||||||
|
"لقد تجاوزت حد الإلغاء المسموح به (3 مرات).\nلا يمكنك العمل حتى انتهاء العقوبة.",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
remainingBlockTimeStr.value, // 🔥 الوقت يتحدث هنا
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
confirm: Obx(() {
|
||||||
|
// الزر يكون مفعلاً فقط عندما ينتهي الوقت
|
||||||
|
bool isFinished = remainingBlockTimeStr.value == "00:00:00" ||
|
||||||
|
remainingBlockTimeStr.value == "Done";
|
||||||
|
return ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: isFinished ? Colors.green : Colors.grey,
|
||||||
|
),
|
||||||
|
onPressed: isFinished
|
||||||
|
? () {
|
||||||
|
Get.back(); // إغلاق الديالوج
|
||||||
|
box.remove(BoxName.blockUntilDate); // إزالة الحظر
|
||||||
|
Get.snackbar("أهلاً بك", "يمكنك الآن استقبال الطلبات",
|
||||||
|
backgroundColor: Colors.green);
|
||||||
|
}
|
||||||
|
: null, // زر معطل
|
||||||
|
child: Text(isFinished ? "Go Online" : "انتظر انتهاء الوقت"),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// الوقت انتهى أصلاً -> تنظيف
|
||||||
|
box.remove(BoxName.blockUntilDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// دالة العداد التنازلي
|
||||||
|
void _startBlockCountdown(DateTime expiry) {
|
||||||
|
_blockTimer?.cancel();
|
||||||
|
_blockTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
|
DateTime now = DateTime.now();
|
||||||
|
if (now.isAfter(expiry)) {
|
||||||
|
// انتهى الوقت
|
||||||
|
remainingBlockTimeStr.value = "Done";
|
||||||
|
timer.cancel();
|
||||||
|
} else {
|
||||||
|
// حساب الفرق وتنسيقه
|
||||||
|
Duration diff = expiry.difference(now);
|
||||||
|
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||||
|
String hours = twoDigits(diff.inHours);
|
||||||
|
String minutes = twoDigits(diff.inMinutes.remainder(60));
|
||||||
|
String seconds = twoDigits(diff.inSeconds.remainder(60));
|
||||||
|
|
||||||
|
remainingBlockTimeStr.value = "$hours:$minutes:$seconds";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
_blockTimer?.cancel();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
void getRefusedOrderByCaptain() async {
|
void getRefusedOrderByCaptain() async {
|
||||||
DateTime today = DateTime.now();
|
DateTime today = DateTime.now();
|
||||||
int todayDay = today.day;
|
int todayDay = today.day;
|
||||||
@@ -142,7 +356,8 @@ class HomeCaptainController extends GetxController {
|
|||||||
await sql.getCustomQuery(customQuery);
|
await sql.getCustomQuery(customQuery);
|
||||||
countRefuse = results[0]['count'].toString();
|
countRefuse = results[0]['count'].toString();
|
||||||
update();
|
update();
|
||||||
if (int.parse(countRefuse) > 3 || double.parse(totalPoints) <= -3000) {
|
if (double.parse(totalPoints) <= -200) {
|
||||||
|
// if (int.parse(countRefuse) > 3 || double.parse(totalPoints) <= -200) {
|
||||||
locationController.stopLocationUpdates();
|
locationController.stopLocationUpdates();
|
||||||
activeStartTime = null;
|
activeStartTime = null;
|
||||||
activeTimer?.cancel();
|
activeTimer?.cancel();
|
||||||
@@ -181,29 +396,25 @@ class HomeCaptainController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// late GoogleMapController mapHomeCaptainController;
|
// late GoogleMapController mapHomeCaptainController;
|
||||||
// void onMapCreated(GoogleMapController controller) {
|
GoogleMapController? mapHomeCaptainController;
|
||||||
// mapHomeCaptainController = controller;
|
// final locationController = Get.find<LocationController>();
|
||||||
// controller.getVisibleRegion();
|
|
||||||
// // Animate camera to user location (optional)
|
|
||||||
// controller.animateCamera(
|
|
||||||
// CameraUpdate.newLatLng(Get.find<LocationController>().myLocation),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
GoogleMapController? mapHomeCaptainController; // Nullable controller
|
|
||||||
|
|
||||||
|
// --- FIX 2: Smart Map Creation ---
|
||||||
void onMapCreated(GoogleMapController controller) {
|
void onMapCreated(GoogleMapController controller) {
|
||||||
mapHomeCaptainController = controller;
|
mapHomeCaptainController = controller;
|
||||||
|
|
||||||
// Optional: Check if the controller is still null (just for safety)
|
// Check actual location before moving camera
|
||||||
if (mapHomeCaptainController != null) {
|
var currentLoc = locationController.myLocation;
|
||||||
// Get the visible region
|
if (currentLoc.latitude != 0 && currentLoc.longitude != 0) {
|
||||||
controller.getVisibleRegion();
|
|
||||||
|
|
||||||
// Animate camera to user location (optional)
|
|
||||||
controller.animateCamera(
|
controller.animateCamera(
|
||||||
CameraUpdate.newLatLng(Get.find<LocationController>().myLocation),
|
CameraUpdate.newLatLng(currentLoc),
|
||||||
);
|
);
|
||||||
} else {}
|
} else {
|
||||||
|
// Optional: Move to default city view instead of ocean
|
||||||
|
controller.animateCamera(
|
||||||
|
CameraUpdate.newLatLngZoom(myLocation, 10),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void savePeriod(Duration period) {
|
void savePeriod(Duration period) {
|
||||||
@@ -235,7 +446,14 @@ class HomeCaptainController extends GetxController {
|
|||||||
getlocation() async {
|
getlocation() async {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
update();
|
update();
|
||||||
await Get.find<LocationController>().getLocation();
|
// This ensures we try to get a fix, but map doesn't crash if it fails
|
||||||
|
await locationController.getLocation();
|
||||||
|
|
||||||
|
var loc = locationController.myLocation;
|
||||||
|
if (loc.latitude != 0) {
|
||||||
|
myLocation = loc;
|
||||||
|
}
|
||||||
|
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
@@ -246,7 +464,7 @@ class HomeCaptainController extends GetxController {
|
|||||||
// isLoading = true;
|
// isLoading = true;
|
||||||
update();
|
update();
|
||||||
|
|
||||||
var res = await CRUD().get(
|
var res = await CRUD().getWallet(
|
||||||
link: AppLink.getDriverPaymentPoints,
|
link: AppLink.getDriverPaymentPoints,
|
||||||
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
payload: {'driverID': box.read(BoxName.driverID).toString()},
|
||||||
);
|
);
|
||||||
@@ -264,8 +482,35 @@ class HomeCaptainController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. دالة نستدعيها عند قبول الطلب
|
||||||
|
void pauseHomeMapUpdates() {
|
||||||
|
isHomeMapActive = false;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. دالة نستدعيها عند العودة للصفحة الرئيسية
|
||||||
|
void resumeHomeMapUpdates() {
|
||||||
|
isHomeMapActive = true;
|
||||||
|
// إنعاش الخريطة عند العودة
|
||||||
|
if (mapHomeCaptainController != null) {
|
||||||
|
onMapCreated(mapHomeCaptainController!);
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() async {
|
void onInit() async {
|
||||||
|
// ✅ طلب الإذونات أولاً
|
||||||
|
bool permissionsGranted = await PermissionsHelper.requestAllPermissions();
|
||||||
|
|
||||||
|
if (permissionsGranted) {
|
||||||
|
// ✅ بدء الخدمة بعد الحصول على الإذونات
|
||||||
|
await BackgroundServiceHelper.startService();
|
||||||
|
print('✅ Background service started successfully');
|
||||||
|
} else {
|
||||||
|
print('❌ لم يتم منح الإذونات - الخدمة لن تعمل');
|
||||||
|
// اعرض رسالة للمستخدم
|
||||||
|
}
|
||||||
// await locationBackController.requestLocationPermission();
|
// await locationBackController.requestLocationPermission();
|
||||||
Get.put(FirebaseMessagesController());
|
Get.put(FirebaseMessagesController());
|
||||||
addToken();
|
addToken();
|
||||||
@@ -279,87 +524,56 @@ class HomeCaptainController extends GetxController {
|
|||||||
getAllPayment();
|
getAllPayment();
|
||||||
startPeriodicExecution();
|
startPeriodicExecution();
|
||||||
getCaptainWalletFromBuyPoints();
|
getCaptainWalletFromBuyPoints();
|
||||||
onMapCreated(mapHomeCaptainController!);
|
// onMapCreated(mapHomeCaptainController!);
|
||||||
// totalPoints = Get.find<CaptainWalletController>().totalPoints.toString();
|
// totalPoints = Get.find<CaptainWalletController>().totalPoints.toString();
|
||||||
getRefusedOrderByCaptain();
|
// getRefusedOrderByCaptain();
|
||||||
|
// 🔥 الفحص عند تشغيل التطبيق
|
||||||
|
checkAndShowBlockDialog();
|
||||||
box.write(BoxName.statusDriverLocation, 'off');
|
box.write(BoxName.statusDriverLocation, 'off');
|
||||||
|
// 2. عدل الليسنر ليصبح مشروطاً
|
||||||
|
locationController.addListener(() {
|
||||||
|
// الشرط الذهبي: إذا كانت الصفحة غير نشطة أو الخريطة غير موجودة، لا تفعل شيئاً
|
||||||
|
if (!isHomeMapActive || mapHomeCaptainController == null || isClosed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
// isActive الخاصة بالزر "متصل/غير متصل"
|
||||||
|
var loc = locationController.myLocation;
|
||||||
|
if (loc.latitude != 0 && loc.longitude != 0) {
|
||||||
|
try {
|
||||||
|
mapHomeCaptainController!.animateCamera(
|
||||||
|
CameraUpdate.newCameraPosition(
|
||||||
|
CameraPosition(
|
||||||
|
target: loc,
|
||||||
|
zoom: 17.5,
|
||||||
|
tilt: 50.0,
|
||||||
|
bearing: locationController.heading,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// التقاط الخطأ بصمت إذا حدث أثناء الانتقال
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
// LocationController().getLocation();
|
// LocationController().getLocation();
|
||||||
super.onInit();
|
super.onInit();
|
||||||
}
|
}
|
||||||
// void getRefusedOrderByCaptain() async {
|
|
||||||
// // Get today's date in YYYY-MM-DD format
|
|
||||||
// String today = DateTime.now().toString().substring(0, 10);
|
|
||||||
|
|
||||||
// String driverId = box.read(BoxName.driverID).toString();
|
|
||||||
|
|
||||||
// String customQuery = '''
|
|
||||||
// SELECT COUNT(*) AS count
|
|
||||||
// FROM ${TableName.driverOrdersRefuse}
|
|
||||||
// WHERE driver_id = '$driverId'
|
|
||||||
// AND DATE(created_at) = '$today'
|
|
||||||
// ''';
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// List<Map<String, dynamic>> results =
|
|
||||||
// await sql.getCustomQuery(customQuery);
|
|
||||||
// countRefuse = results[0]['count'].toString();
|
|
||||||
// update();
|
|
||||||
// if (int.parse(countRefuse) > 3) {
|
|
||||||
// box.write(BoxName.statusDriverLocation, 'on');
|
|
||||||
// locationController.stopLocationUpdates();
|
|
||||||
// Get.defaultDialog(
|
|
||||||
// // backgroundColor: CupertinoColors.destructiveRed,
|
|
||||||
// barrierDismissible: false,
|
|
||||||
// title: 'You Are Stopped For this Day !'.tr,
|
|
||||||
// content: Text(
|
|
||||||
// 'You Refused 3 Rides this Day that is the reason \nSee you Tomorrow!'
|
|
||||||
// .tr,
|
|
||||||
// style: AppStyle.title,
|
|
||||||
// ),
|
|
||||||
// confirm: MyElevatedButton(
|
|
||||||
// title: 'Ok , See you Tomorrow'.tr,
|
|
||||||
// onPressed: () => Get.back()));
|
|
||||||
// } else {
|
|
||||||
// box.write(BoxName.statusDriverLocation, 'off');
|
|
||||||
// }
|
|
||||||
// } catch (e) {}
|
|
||||||
// }
|
|
||||||
|
|
||||||
addToken() async {
|
addToken() async {
|
||||||
String? fingerPrint = await storage.read(key: BoxName.fingerPrint);
|
String? fingerPrint = await storage.read(key: BoxName.fingerPrint);
|
||||||
CRUD().post(link: AppLink.addTokensDriver, payload: {
|
final payload = {
|
||||||
'token': (box.read(BoxName.tokenDriver)),
|
'token': (box.read(BoxName.tokenDriver)),
|
||||||
'captain_id': (box.read(BoxName.driverID)).toString(),
|
'captain_id': (box.read(BoxName.driverID)).toString(),
|
||||||
'fingerPrint': (fingerPrint).toString()
|
'fingerPrint': (fingerPrint).toString()
|
||||||
});
|
};
|
||||||
|
// Log.print('payload: ${payload}');
|
||||||
// CRUD().post(
|
CRUD().post(link: AppLink.addTokensDriver, payload: payload);
|
||||||
// link: "${AppLink.seferAlexandriaServer}/ride/firebase/addDriver.php",
|
|
||||||
// payload: {
|
|
||||||
// 'token': box.read(BoxName.tokenDriver),
|
|
||||||
// 'captain_id': box.read(BoxName.driverID).toString(),
|
|
||||||
// 'fingerPrint': (fingerPrint).toString()
|
|
||||||
// });
|
|
||||||
// CRUD().post(
|
|
||||||
// link: "${AppLink.seferGizaServer}/ride/firebase/addDriver.php",
|
|
||||||
// payload: {
|
|
||||||
// 'token': box.read(BoxName.tokenDriver),
|
|
||||||
// 'captain_id': box.read(BoxName.driverID).toString(),
|
|
||||||
// 'fingerPrint': (fingerPrint).toString()
|
|
||||||
// });
|
|
||||||
await CRUD().postWallet(
|
|
||||||
link: "${AppLink.seferPaymentServer}/ride/firebase/addDriver.php",
|
|
||||||
payload: {
|
|
||||||
'token': box.read(BoxName.tokenDriver),
|
|
||||||
'captain_id': box.read(BoxName.driverID).toString(),
|
|
||||||
'fingerPrint': (fingerPrint).toString()
|
|
||||||
});
|
|
||||||
// MapDriverController().driverCallPassenger();
|
|
||||||
// box.write(BoxName.statusDriverLocation, 'off');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPaymentToday() async {
|
getPaymentToday() async {
|
||||||
var res = await CRUD().get(
|
var res = await CRUD().getWallet(
|
||||||
link: AppLink.getDriverPaymentToday,
|
link: AppLink.getDriverPaymentToday,
|
||||||
payload: {'driverID': box.read(BoxName.driverID).toString()});
|
payload: {'driverID': box.read(BoxName.driverID).toString()});
|
||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
@@ -423,7 +637,7 @@ class HomeCaptainController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAllPayment() async {
|
getAllPayment() async {
|
||||||
var res = await CRUD().get(
|
var res = await CRUD().getWallet(
|
||||||
link: AppLink.getAllPaymentFromRide,
|
link: AppLink.getAllPaymentFromRide,
|
||||||
payload: {'driverID': box.read(BoxName.driverID).toString()});
|
payload: {'driverID': box.read(BoxName.driverID).toString()});
|
||||||
if (res == 'failure') {
|
if (res == 'failure') {
|
||||||
@@ -444,12 +658,25 @@ class HomeCaptainController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getCaptainDurationOnToday() async {
|
Future<void> getCaptainDurationOnToday() async {
|
||||||
|
try {
|
||||||
var res = await CRUD().get(
|
var res = await CRUD().get(
|
||||||
link: AppLink.getTotalDriverDurationToday,
|
link: AppLink.getTotalDriverDurationToday,
|
||||||
payload: {'driver_id': box.read(BoxName.driverID).toString()});
|
payload: {'driver_id': box.read(BoxName.driverID).toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res == null || res == 'failure') {
|
||||||
|
totalDurationToday = '0';
|
||||||
|
update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = jsonDecode(res);
|
||||||
|
totalDurationToday = data['message']?[0]?['total_duration'] ?? '0';
|
||||||
|
} catch (e) {
|
||||||
|
print('Error in getCaptainDurationOnToday: $e');
|
||||||
|
totalDurationToday = '0';
|
||||||
|
}
|
||||||
|
|
||||||
data = jsonDecode(res);
|
|
||||||
totalDurationToday = data['message'][0]['total_duration'];
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,6 +684,7 @@ class HomeCaptainController extends GetxController {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
activeTimer?.cancel();
|
activeTimer?.cancel();
|
||||||
stopTimer();
|
stopTimer();
|
||||||
|
mapHomeCaptainController?.dispose(); // Dispose controller
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,282 +1,617 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sefer_driver/constant/links.dart';
|
|
||||||
import 'package:sefer_driver/main.dart';
|
|
||||||
import 'package:google_maps_flutter/google_maps_flutter.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 'dart:math' as math;
|
||||||
import '../../../constant/box_name.dart';
|
|
||||||
import '../../../print.dart';
|
|
||||||
import '../../functions/audio_controller.dart';
|
|
||||||
import '../../functions/crud.dart';
|
|
||||||
import '../../functions/encrypt_decrypt.dart';
|
|
||||||
import '../../functions/location_controller.dart';
|
|
||||||
import 'home_captain_controller.dart';
|
|
||||||
|
|
||||||
class OrderRequestController extends GetxController {
|
import '../../../constant/box_name.dart';
|
||||||
double progress = 0;
|
import '../../../constant/links.dart';
|
||||||
double progressSpeed = 0;
|
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 '../../firebase/local_notification.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 duration = 15;
|
||||||
int durationSpeed = 20;
|
int remainingTime = 15;
|
||||||
int remainingTime = 0;
|
Timer? _timer;
|
||||||
int remainingTimeSpeed = 0;
|
|
||||||
String countRefuse = '0';
|
|
||||||
bool applied = false;
|
bool applied = false;
|
||||||
final locationController = Get.put(LocationController());
|
final locationController = Get.put(LocationController());
|
||||||
BitmapDescriptor startIcon = BitmapDescriptor.defaultMarker;
|
|
||||||
BitmapDescriptor endIcon = BitmapDescriptor.defaultMarker;
|
// 🔥 متغير لمنع تكرار القبول
|
||||||
final arguments = Get.arguments;
|
bool _isRideTakenHandled = false;
|
||||||
var myList;
|
|
||||||
late int hours;
|
// --- الأيقونات والماركرز ---
|
||||||
late int minutes;
|
BitmapDescriptor? driverIcon;
|
||||||
GoogleMapController? mapController; // Make it nullable
|
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
|
@override
|
||||||
Future<void> onInit() async {
|
Future<void> onInit() async {
|
||||||
print('OrderRequestController onInit called');
|
// 🛑 حماية من الفتح المتكرر لنفس الطلب
|
||||||
await initializeOrderPage();
|
if (Get.arguments == null) {
|
||||||
bool isOverlayActive = await FlutterOverlayWindow.isActive();
|
print("❌ OrderController Error: No arguments received.");
|
||||||
if (isOverlayActive) {
|
Get.back(); // إغلاق الصفحة فوراً
|
||||||
await FlutterOverlayWindow.closeOverlay();
|
return;
|
||||||
}
|
}
|
||||||
addCustomStartIcon();
|
super.onInit();
|
||||||
addCustomEndIcon();
|
WidgetsBinding.instance.addObserver(this);
|
||||||
startTimer(
|
|
||||||
myList[6].toString(),
|
_checkOverlay();
|
||||||
myList[16].toString(),
|
|
||||||
|
// 🔥 تهيئة البيانات هي الخطوة الأولى والأهم
|
||||||
|
_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();
|
update();
|
||||||
super.onInit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
late LatLngBounds bounds;
|
|
||||||
late List<LatLng> pointsDirection;
|
|
||||||
late String body;
|
|
||||||
late double latPassengerLocation;
|
|
||||||
late double lngPassengerLocation;
|
|
||||||
late double lngPassengerDestination;
|
|
||||||
late double latPassengerDestination;
|
|
||||||
|
|
||||||
Future<void> initializeOrderPage() async {
|
|
||||||
final myListString = Get.arguments['myListString'];
|
|
||||||
|
|
||||||
if (Get.arguments['DriverList'] == null ||
|
|
||||||
Get.arguments['DriverList'].isEmpty) {
|
|
||||||
myList = jsonDecode(myListString);
|
|
||||||
Log.print('myList from myListString: ${myList}');
|
|
||||||
} else {
|
|
||||||
myList = Get.arguments['DriverList'];
|
|
||||||
Log.print('myList from DriverList: ${myList}');
|
|
||||||
}
|
|
||||||
|
|
||||||
body = Get.arguments['body'];
|
|
||||||
Duration durationToAdd =
|
|
||||||
Duration(seconds: (double.tryParse(myList[4]) ?? 0).toInt());
|
|
||||||
hours = durationToAdd.inHours;
|
|
||||||
minutes = (durationToAdd.inMinutes % 60).round();
|
|
||||||
startTimerSpeed(myList[6].toString(), body.toString());
|
|
||||||
|
|
||||||
// --- Using the provided logic for initialization ---
|
|
||||||
var cords = myList[0].toString().split(',');
|
|
||||||
var cordDestination = myList[1].toString().split(',');
|
|
||||||
|
|
||||||
double? parseDouble(String value) {
|
|
||||||
try {
|
|
||||||
return double.parse(value);
|
|
||||||
} catch (e) {
|
|
||||||
Log.print("Error parsing value: $value");
|
|
||||||
return null; // or handle the error appropriately
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
latPassengerLocation = parseDouble(cords[0]) ?? 0.0;
|
|
||||||
lngPassengerLocation = parseDouble(cords[1]) ?? 0.0;
|
|
||||||
latPassengerDestination = parseDouble(cordDestination[0]) ?? 0.0;
|
|
||||||
lngPassengerDestination = parseDouble(cordDestination[1]) ?? 0.0;
|
|
||||||
|
|
||||||
pointsDirection = [
|
|
||||||
LatLng(latPassengerLocation, lngPassengerLocation),
|
|
||||||
LatLng(latPassengerDestination, lngPassengerDestination)
|
|
||||||
];
|
|
||||||
Log.print('pointsDirection: $pointsDirection');
|
|
||||||
|
|
||||||
calculateBounds();
|
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onMapCreated(GoogleMapController controller) {
|
void onMapCreated(GoogleMapController controller) {
|
||||||
mapController = controller;
|
mapController = controller;
|
||||||
animateCameraToBounds();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void calculateBounds() {
|
// --- قبول الطلب وإدارة التايمر ---
|
||||||
double minLat = math.min(latPassengerLocation, latPassengerDestination);
|
void startTimer() {
|
||||||
double maxLat = math.max(latPassengerLocation, latPassengerDestination);
|
_timer?.cancel();
|
||||||
double minLng = math.min(lngPassengerLocation, lngPassengerDestination);
|
remainingTime = duration;
|
||||||
double maxLng = math.max(lngPassengerLocation, lngPassengerDestination);
|
_playAudio();
|
||||||
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
bounds = LatLngBounds(
|
if (remainingTime <= 0) {
|
||||||
southwest: LatLng(minLat, minLng),
|
timer.cancel();
|
||||||
northeast: LatLng(maxLat, maxLng),
|
_stopAudio();
|
||||||
);
|
if (!applied) Get.back();
|
||||||
Log.print('Calculated Bounds: $bounds');
|
|
||||||
}
|
|
||||||
|
|
||||||
void animateCameraToBounds() {
|
|
||||||
if (mapController != null) {
|
|
||||||
mapController!.animateCamera(CameraUpdate.newLatLngBounds(bounds, 80.0));
|
|
||||||
} else {
|
} else {
|
||||||
Log.print('mapController is null, cannot animate camera.');
|
remainingTime--;
|
||||||
|
progress = remainingTime / duration;
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
getRideDEtailsForBackgroundOrder(String rideId) async {
|
|
||||||
await CRUD().get(link: AppLink.getRidesDetails, payload: {
|
|
||||||
'id': rideId,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void addCustomStartIcon() async {
|
void endTimer() => _timer?.cancel();
|
||||||
ImageConfiguration config = const ImageConfiguration(size: Size(30, 30));
|
void changeApplied() => applied = true;
|
||||||
BitmapDescriptor.asset(
|
|
||||||
config,
|
void _playAudio() async {
|
||||||
'assets/images/A.png',
|
try {
|
||||||
).then((value) {
|
await audioPlayer.setAsset('assets/order.mp3', preload: true);
|
||||||
startIcon = value;
|
await audioPlayer.setLoopMode(LoopMode.one);
|
||||||
update();
|
await audioPlayer.play();
|
||||||
});
|
} catch (e) {
|
||||||
|
print(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void addCustomEndIcon() {
|
void _stopAudio() => audioPlayer.stop();
|
||||||
ImageConfiguration config = const ImageConfiguration(size: Size(30, 30));
|
|
||||||
BitmapDescriptor.asset(
|
|
||||||
config,
|
|
||||||
'assets/images/b.png',
|
|
||||||
).then((value) {
|
|
||||||
endIcon = value;
|
|
||||||
update();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void changeApplied() {
|
void _listenForRideTaken() {
|
||||||
applied = true;
|
if (locationController.socket != null) {
|
||||||
update();
|
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();
|
||||||
|
|
||||||
double mpg = 0;
|
if (takenRideId == myCurrentRideId && whoTookIt != myDriverId) {
|
||||||
calculateConsumptionFuel() {
|
_isRideTakenHandled = true;
|
||||||
mpg = Get.find<HomeCaptainController>().fuelPrice / 12;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _timerActive = false;
|
|
||||||
|
|
||||||
Future<void> startTimer(String driverID, String orderID) async {
|
|
||||||
_timerActive = true;
|
|
||||||
for (int i = 0; i <= duration && _timerActive; i++) {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
progress = i / duration;
|
|
||||||
remainingTime = duration - i;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
if (remainingTime == 0 && _timerActive) {
|
|
||||||
if (applied == false) {
|
|
||||||
endTimer();
|
endTimer();
|
||||||
refuseOrder(orderID);
|
// 1. حذف الإشعار من شريط التنبيهات فوراً
|
||||||
}
|
NotificationController().cancelOrderNotification();
|
||||||
}
|
if (Get.isSnackbarOpen) Get.closeCurrentSnackbar();
|
||||||
}
|
if (Get.isDialogOpen ?? false) Get.back();
|
||||||
|
|
||||||
void endTimer() {
|
|
||||||
_timerActive = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void startTimerSpeed(String driverID, orderID) async {
|
|
||||||
for (int i = 0; i <= durationSpeed; i++) {
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
progressSpeed = i / durationSpeed;
|
|
||||||
remainingTimeSpeed = durationSpeed - i;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
if (remainingTimeSpeed == 0) {
|
|
||||||
if (applied == false) {
|
|
||||||
Get.back();
|
Get.back();
|
||||||
|
Get.snackbar("تنبيه", "تم قبول الطلب من قبل سائق آخر",
|
||||||
|
backgroundColor: Colors.orange, colorText: Colors.white);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void refuseOrder(
|
// Lifecycle
|
||||||
orderID,
|
@override
|
||||||
) async {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
await CRUD().postFromDialogue(link: AppLink.addDriverOrder, payload: {
|
super.didChangeAppLifecycleState(state);
|
||||||
'driver_id': box.read(BoxName.driverID),
|
if (state == AppLifecycleState.paused ||
|
||||||
'order_id': (orderID),
|
state == AppLifecycleState.detached) {
|
||||||
'status': 'Refused'
|
isInBackground = true;
|
||||||
});
|
} else if (state == AppLifecycleState.resumed) {
|
||||||
await CRUD().post(link: AppLink.updateRides, payload: {
|
isInBackground = false;
|
||||||
'id': (orderID),
|
FlutterOverlayWindow.closeOverlay();
|
||||||
'status': 'Refused',
|
|
||||||
'driver_id': box.read(BoxName.driverID),
|
|
||||||
});
|
|
||||||
if (AppLink.endPoint != AppLink.seferCairoServer) {
|
|
||||||
CRUD().post(link: '${AppLink.endPoint}/rides/update.php', payload: {
|
|
||||||
'id': (orderID),
|
|
||||||
'status': 'Refused',
|
|
||||||
'driver_id': box.read(BoxName.driverID),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addRideToNotificationDriverString(
|
void _checkOverlay() async {
|
||||||
orderID,
|
if (Platform.isAndroid && await FlutterOverlayWindow.isActive()) {
|
||||||
String startLocation,
|
await FlutterOverlayWindow.closeOverlay();
|
||||||
String endLocation,
|
|
||||||
String date,
|
|
||||||
String time,
|
|
||||||
String price,
|
|
||||||
String passengerId,
|
|
||||||
String status,
|
|
||||||
String carType,
|
|
||||||
String passengerRate,
|
|
||||||
String priceForPassenger,
|
|
||||||
String distance,
|
|
||||||
String duration,
|
|
||||||
) async {
|
|
||||||
await CRUD().post(link: AppLink.addWaitingRide, payload: {
|
|
||||||
'id': (orderID),
|
|
||||||
'start_location': startLocation,
|
|
||||||
'end_location': endLocation,
|
|
||||||
'date': date,
|
|
||||||
'time': time,
|
|
||||||
'price': price,
|
|
||||||
'passenger_id': (passengerId),
|
|
||||||
'status': status,
|
|
||||||
'carType': carType,
|
|
||||||
'passengerRate': passengerRate,
|
|
||||||
'price_for_passenger': priceForPassenger,
|
|
||||||
'distance': distance,
|
|
||||||
'duration': duration,
|
|
||||||
});
|
|
||||||
if (AppLink.endPoint != AppLink.seferCairoServer) {
|
|
||||||
CRUD().post(
|
|
||||||
link: '${AppLink.endPoint}/notificationCaptain/addWaitingRide.php',
|
|
||||||
payload: {
|
|
||||||
'id': (orderID),
|
|
||||||
'start_location': startLocation,
|
|
||||||
'end_location': endLocation,
|
|
||||||
'date': date,
|
|
||||||
'time': time,
|
|
||||||
'price': price,
|
|
||||||
'passenger_id': (passengerId),
|
|
||||||
'status': status,
|
|
||||||
'carType': carType,
|
|
||||||
'passengerRate': passengerRate,
|
|
||||||
'price_for_passenger': priceForPassenger,
|
|
||||||
'distance': distance,
|
|
||||||
'duration': duration,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accept Order Logic
|
||||||
|
Future<void> acceptOrder() async {
|
||||||
|
endTimer();
|
||||||
|
_stopAudio();
|
||||||
|
|
||||||
|
// 1. إرسال الطلب
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
|
||||||
|
Log.print('res from orderrequestpage: ${res}');
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// تصحيح: فحص الرد بدقة (Map أو String)
|
||||||
|
// ============================================================
|
||||||
|
bool isFailure = false;
|
||||||
|
|
||||||
|
if (res is Map && res['status'] == 'failure') {
|
||||||
|
isFailure = true;
|
||||||
|
} else if (res == 'failure') {
|
||||||
|
isFailure = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFailure) {
|
||||||
|
// ⛔ حالة الفشل: الطلب مأخوذ
|
||||||
|
MyDialog().getDialog("عذراً، الطلب أخذه سائق آخر.", '', () {
|
||||||
|
Get.back(); // إغلاق الديالوج
|
||||||
|
Get.back(); // العودة للصفحة الرئيسية (إغلاق صفحة الطلب)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// ✅ حالة النجاح
|
||||||
|
|
||||||
|
// حماية من الكراش: التأكد من وجود HomeCaptainController قبل استخدامه
|
||||||
|
if (!Get.isRegistered<HomeCaptainController>()) {
|
||||||
|
Get.put(HomeCaptainController());
|
||||||
|
} else {
|
||||||
|
Get.find<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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
lib/controller/home/navigation/decode_polyline_isolate.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
|
||||||
|
// تم تعديل الدالة لتقبل وسيط من نوع `dynamic` لحل مشكلة عدم تطابق الأنواع مع دالة `compute`.
|
||||||
|
// هذه الدالة لا تزال تعمل كدالة من المستوى الأعلى (Top-level function)
|
||||||
|
// وهو شرط أساسي لاستخدامها مع دالة compute.
|
||||||
|
List<LatLng> decodePolylineIsolate(dynamic encodedMessage) {
|
||||||
|
// التأكد من أن الرسالة المستقبلة هي من نوع String
|
||||||
|
if (encodedMessage is! String) {
|
||||||
|
// إرجاع قائمة فارغة أو إظهار خطأ إذا كان النوع غير صحيح
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
final String encoded = encodedMessage;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,19 +1,22 @@
|
|||||||
// lib/views/navigation_view.dart
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
import 'dart:ui';
|
import 'dart:ui'; // For BackdropFilter
|
||||||
|
|
||||||
import 'navigation_controller.dart'; // For BackdropFilter
|
import 'navigation_controller.dart';
|
||||||
|
|
||||||
// استخدام نفس مسار الاستيراد الذي قدمته
|
// ملاحظة: افترضتُ أن لديك لوناً أساسياً في هذا الملف
|
||||||
|
// import 'package:sefer_driver/constant/colors.dart';
|
||||||
|
// سأستخدم اللون الأزرق كبديل مؤقت
|
||||||
|
const Color kPrimaryColor = Color(0xFF0D47A1);
|
||||||
|
|
||||||
class NavigationView extends StatelessWidget {
|
class NavigationView extends StatelessWidget {
|
||||||
const NavigationView({super.key});
|
const NavigationView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// استخدام Get.find() بدلاً من Get.put() لضمان أن الكونترولر مُهيأ مسبقاً
|
||||||
|
// إذا كانت هذه هي نقطة الدخول، Get.put() صحيح.
|
||||||
final NavigationController controller = Get.put(NavigationController());
|
final NavigationController controller = Get.put(NavigationController());
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -23,7 +26,6 @@ class NavigationView extends StatelessWidget {
|
|||||||
// --- الخريطة ---
|
// --- الخريطة ---
|
||||||
GoogleMap(
|
GoogleMap(
|
||||||
onMapCreated: controller.onMapCreated,
|
onMapCreated: controller.onMapCreated,
|
||||||
// --- السطر المضاف والمهم هنا ---
|
|
||||||
onLongPress: controller.onMapLongPressed,
|
onLongPress: controller.onMapLongPressed,
|
||||||
initialCameraPosition: CameraPosition(
|
initialCameraPosition: CameraPosition(
|
||||||
target: controller.myLocation ??
|
target: controller.myLocation ??
|
||||||
@@ -35,29 +37,35 @@ class NavigationView extends StatelessWidget {
|
|||||||
myLocationEnabled: false,
|
myLocationEnabled: false,
|
||||||
myLocationButtonEnabled: false,
|
myLocationButtonEnabled: false,
|
||||||
compassEnabled: false,
|
compassEnabled: false,
|
||||||
zoomControlsEnabled: false,
|
zoomControlsEnabled: false, buildingsEnabled: false,
|
||||||
// تعديل الـ padding لإعطاء مساحة للعناصر العلوية والسفلية
|
// تعديل الـ padding لإعطاء مساحة للعناصر العلوية والسفلية
|
||||||
|
// مساحة أكبر في الأعلى للبحث + النتائج، ومساحة أكبر بالأسفل للملاحة
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
bottom: controller.currentInstruction.isNotEmpty ? 130 : 0,
|
bottom: controller.currentInstruction.isNotEmpty ? 170 : 0,
|
||||||
top: 140),
|
top: 150,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// --- واجهة البحث ونتائجه ---
|
// --- واجهة البحث (تصميم زجاجي) ---
|
||||||
_buildSearchUI(controller),
|
_buildGlassSearchUI(controller),
|
||||||
|
|
||||||
// --- إرشادات الملاحة المطورة ---
|
// --- إرشادات الملاحة (تصميم عائم) ---
|
||||||
if (controller.currentInstruction.isNotEmpty)
|
if (controller.currentInstruction.isNotEmpty)
|
||||||
_buildNavigationInstruction(controller),
|
_buildFloatingNavigationUI(controller),
|
||||||
|
|
||||||
// --- أزرار التحكم على الخريطة ---
|
// --- أزرار التحكم (تصميم عائم) ---
|
||||||
_buildMapControls(controller),
|
_buildFloatingMapControls(controller),
|
||||||
|
|
||||||
// --- مؤشر التحميل ---
|
// --- مؤشر التحميل ---
|
||||||
if (controller.isLoading)
|
if (controller.isLoading)
|
||||||
Container(
|
Container(
|
||||||
color: Colors.black.withOpacity(0.5),
|
color: Colors.black.withOpacity(0.5),
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: CircularProgressIndicator(color: Colors.white)),
|
child: CircularProgressIndicator(
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -65,94 +73,75 @@ class NavigationView extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ويدجت خاصة بواجهة البحث ---
|
/// --- 1. واجهة البحث بالتصميم الزجاجي المطور ---
|
||||||
Widget _buildSearchUI(NavigationController controller) {
|
Widget _buildGlassSearchUI(NavigationController controller) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
// --- شريط البحث ---
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(28.0),
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
|
||||||
|
child: Container(
|
||||||
|
height: 56,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white.withOpacity(0.85),
|
||||||
borderRadius: BorderRadius.circular(15.0),
|
borderRadius: BorderRadius.circular(28.0),
|
||||||
|
border: Border.all(color: Colors.white.withOpacity(0.4)),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.15),
|
color: Colors.black.withOpacity(0.05),
|
||||||
blurRadius: 10,
|
blurRadius: 15,
|
||||||
offset: const Offset(0, 5),
|
offset: const Offset(0, 5),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(left: 18.0, right: 10.0),
|
||||||
|
child: Icon(Icons.search,
|
||||||
|
color: kPrimaryColor, size: 24),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller.placeDestinationController,
|
controller: controller.placeDestinationController,
|
||||||
onChanged: (val) {
|
onChanged: controller.onSearchChanged,
|
||||||
controller.onSearchChanged(val);
|
textInputAction: TextInputAction.search,
|
||||||
},
|
style: const TextStyle(
|
||||||
|
fontSize: 16, color: Colors.black87),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'إلى أين تريد الذهاب؟',
|
hintText: 'إلى أين تريد الذهاب؟',
|
||||||
prefixIcon: const Icon(Icons.search, color: Colors.grey),
|
hintStyle: const TextStyle(
|
||||||
suffixIcon: controller
|
color: Colors.black45, fontSize: 16),
|
||||||
.placeDestinationController.text.isNotEmpty
|
|
||||||
? IconButton(
|
|
||||||
icon: const Icon(Icons.clear, color: Colors.grey),
|
|
||||||
onPressed: () {
|
|
||||||
controller.placeDestinationController.clear();
|
|
||||||
controller.placesDestination = [];
|
|
||||||
controller.update();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: (controller.polylines.isNotEmpty
|
|
||||||
? IconButton(
|
|
||||||
icon:
|
|
||||||
const Icon(Icons.close, color: Colors.red),
|
|
||||||
tooltip: 'إلغاء المسار',
|
|
||||||
onPressed: () => controller.clearRoute(),
|
|
||||||
)
|
|
||||||
: null),
|
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.only(bottom: 2),
|
||||||
horizontal: 20, vertical: 15),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
// زر المسح أو إلغاء المسار
|
||||||
|
if (controller
|
||||||
|
.placeDestinationController.text.isNotEmpty)
|
||||||
|
_buildClearButton(controller)
|
||||||
|
else if (controller.polylines.isNotEmpty)
|
||||||
|
_buildCancelRouteButton(controller),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
// --- قائمة النتائج ---
|
||||||
if (controller.placesDestination.isNotEmpty)
|
if (controller.placesDestination.isNotEmpty)
|
||||||
ClipRRect(
|
_buildSearchResultsList(controller),
|
||||||
borderRadius: BorderRadius.circular(15.0),
|
|
||||||
child: BackdropFilter(
|
|
||||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
|
||||||
child: Container(
|
|
||||||
constraints: const BoxConstraints(maxHeight: 220),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.85),
|
|
||||||
borderRadius: BorderRadius.circular(15.0),
|
|
||||||
),
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: controller.placesDestination.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final place = controller.placesDestination[index];
|
|
||||||
return ListTile(
|
|
||||||
title: Text(place['name'] ?? 'اسم غير معروف',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold)),
|
|
||||||
subtitle: Text(place['address'] ?? '',
|
|
||||||
maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
||||||
leading: const Icon(Icons.location_on_outlined,
|
|
||||||
color: Colors.blue),
|
|
||||||
onTap: () => controller.selectDestination(place),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -160,28 +149,136 @@ class NavigationView extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ويدجت خاصة بأزرار التحكم ---
|
Widget _buildClearButton(NavigationController controller) {
|
||||||
Widget _buildMapControls(NavigationController controller) {
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.clear, color: Colors.grey, size: 22),
|
||||||
|
onPressed: () {
|
||||||
|
controller.placeDestinationController.clear();
|
||||||
|
controller.placesDestination = [];
|
||||||
|
controller.update();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCancelRouteButton(NavigationController controller) {
|
||||||
|
return IconButton(
|
||||||
|
tooltip: 'إلغاء المسار',
|
||||||
|
icon: const Icon(Icons.close, color: Colors.redAccent, size: 22),
|
||||||
|
onPressed: () => controller.clearRoute(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSearchResultsList(NavigationController controller) {
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(24.0),
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
|
||||||
|
child: Container(
|
||||||
|
constraints: const BoxConstraints(maxHeight: 220),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.85),
|
||||||
|
borderRadius: BorderRadius.circular(24.0),
|
||||||
|
border: Border.all(color: Colors.white.withOpacity(0.4)),
|
||||||
|
),
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
itemCount: controller.placesDestination.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final place = controller.placesDestination[index];
|
||||||
|
final distance = place['distanceKm'] as double?;
|
||||||
|
final address = (place['address'] ?? '').toString();
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => controller.selectDestination(place),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0, vertical: 12.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// أيقونة الموقع
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: kPrimaryColor.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.location_on_outlined,
|
||||||
|
color: kPrimaryColor, size: 20),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
// الاسم والعنوان
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
place['name'] ?? 'اسم غير معروف',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.black87),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (address.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
address,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black54, fontSize: 13),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
// المسافة
|
||||||
|
if (distance != null)
|
||||||
|
Text(
|
||||||
|
'${distance.toStringAsFixed(1)} كم',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: kPrimaryColor,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// --- 2. أزرار التحكم بالتصميم العائم ---
|
||||||
|
Widget _buildFloatingMapControls(NavigationController controller) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: controller.currentInstruction.isNotEmpty ? 150 : 20,
|
// اجعلها تطفو فوق لوحة الملاحة
|
||||||
right: 12,
|
bottom: controller.currentInstruction.isNotEmpty ? 190 : 24,
|
||||||
|
right: 16,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (controller.polylines.isNotEmpty) ...[
|
if (controller.polylines.isNotEmpty) ...[
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
heroTag: 'rerouteBtn',
|
heroTag: 'rerouteBtn',
|
||||||
mini: true,
|
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
tooltip: 'إعادة حساب المسار',
|
elevation: 6,
|
||||||
onPressed: () => controller.recalculateRoute(),
|
onPressed: () => controller.recalculateRoute(),
|
||||||
child: const Icon(Icons.sync_alt, color: Colors.blue),
|
tooltip: 'إعادة حساب المسار',
|
||||||
|
child: const Icon(Icons.sync_alt, color: kPrimaryColor, size: 24),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 12),
|
||||||
],
|
],
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
heroTag: 'gpsBtn',
|
heroTag: 'gpsBtn',
|
||||||
mini: true,
|
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 6,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (controller.myLocation != null) {
|
if (controller.myLocation != null) {
|
||||||
controller.animateCameraToPosition(
|
controller.animateCameraToPosition(
|
||||||
@@ -191,102 +288,134 @@ class NavigationView extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.gps_fixed, color: Colors.black54),
|
child: const Icon(Icons.gps_fixed, color: Colors.black54, size: 24),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ويدجت خاصة بإرشادات الطريق المطورة ---
|
/// --- 3. واجهة الملاحة بالتصميم العائم المطور ---
|
||||||
Widget _buildNavigationInstruction(NavigationController controller) {
|
Widget _buildFloatingNavigationUI(NavigationController controller) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: 0,
|
bottom: 16,
|
||||||
left: 0,
|
left: 16,
|
||||||
right: 0,
|
right: 16,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: const LinearGradient(
|
||||||
colors: [Colors.blue.shade900, Colors.blue.shade600],
|
colors: [Color(0xFF1E88E5), Color(0xFF0D47A1)], // أزرق متدرج
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomCenter,
|
||||||
),
|
),
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.2),
|
color: Colors.black.withOpacity(0.3),
|
||||||
blurRadius: 15,
|
blurRadius: 25,
|
||||||
offset: const Offset(0, -5),
|
offset: const Offset(0, 10),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
borderRadius: const BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(20),
|
|
||||||
topRight: Radius.circular(20),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 20),
|
padding: const EdgeInsets.fromLTRB(22, 20, 22, 22),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// --- الصف العلوي: السرعة والمسافة ---
|
// --- الصف العلوي: الإرشاد والمسافة ---
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
|
// الأيقونة
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.navigation_rounded,
|
||||||
|
color: Colors.white, size: 28),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
// الإرشاد
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
controller.currentInstruction,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
// المسافة
|
||||||
Text(
|
Text(
|
||||||
controller.distanceToNextStep,
|
controller.distanceToNextStep,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 28,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- فاصل ---
|
||||||
|
if (controller.nextInstruction.isNotEmpty ||
|
||||||
|
controller.currentSpeed > 0)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 14.0),
|
||||||
|
child: Divider(color: Colors.white30, height: 1),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- الصف السفلي: الإرشاد التالي والسرعة ---
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
// الإرشاد التالي
|
||||||
|
Expanded(
|
||||||
|
child: controller.nextInstruction.isNotEmpty
|
||||||
|
? Text(
|
||||||
|
'التالي: ${controller.nextInstruction}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: const SizedBox(), // يترك مساحة فارغة إذا لم يكن هناك إرشاد تالي
|
||||||
|
),
|
||||||
|
|
||||||
|
// السرعة
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
controller.currentSpeed.toStringAsFixed(0),
|
controller.currentSpeed.toStringAsFixed(0),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 28,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
const Text(
|
const Text(
|
||||||
"كم/س",
|
'كم/س',
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 14),
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(color: Colors.white38, height: 20, thickness: 0.8),
|
|
||||||
// --- الصف السفلي: الإرشاد القادم ---
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.navigation_rounded,
|
|
||||||
color: Colors.white, size: 32),
|
|
||||||
const SizedBox(width: 15),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text("الخطوة التالية",
|
|
||||||
style:
|
|
||||||
TextStyle(color: Colors.white70, fontSize: 12)),
|
|
||||||
Text(
|
|
||||||
controller.nextInstruction.isNotEmpty
|
|
||||||
? controller.nextInstruction
|
|
||||||
: controller.currentInstruction,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.bold),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
145
lib/controller/home/navigation/route_matcher_worker.dart
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// lib/controllers/navigation/route_matcher_worker.dart
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
/// Worker entrypoint (spawnUri/spawn).
|
||||||
|
/// Messages:
|
||||||
|
/// - init: {'type':'init','coords': Float64List}
|
||||||
|
/// - match: {'type':'match','id': int, 'lat': double, 'lng': double, 'lastIndex': int, 'window': int}
|
||||||
|
/// - dispose: {'type':'dispose'}
|
||||||
|
///
|
||||||
|
/// Responses are sent back as Map via SendPort:
|
||||||
|
/// - {'type':'ready'}
|
||||||
|
/// - {'type':'matchResult','id': id, 'index': overallIndex, 'lat': lat, 'lng': lng, 'dist': meters}
|
||||||
|
void routeMatcherIsolateEntry(SendPort sendPort) {
|
||||||
|
final ReceivePort port = ReceivePort();
|
||||||
|
sendPort.send({'type': 'ready', 'port': port.sendPort});
|
||||||
|
|
||||||
|
Float64List? flat; // [lat,lng,lat,lng,...]
|
||||||
|
int nPoints = 0;
|
||||||
|
|
||||||
|
port.listen((dynamic message) {
|
||||||
|
try {
|
||||||
|
if (message is Map<String, dynamic>) {
|
||||||
|
final type = message['type'] as String? ?? '';
|
||||||
|
if (type == 'init') {
|
||||||
|
final data = message['coords'] as Float64List?;
|
||||||
|
if (data != null) {
|
||||||
|
flat = data;
|
||||||
|
nPoints = flat!.length ~/ 2;
|
||||||
|
sendPort.send({'type': 'inited', 'points': nPoints});
|
||||||
|
} else {
|
||||||
|
sendPort.send({'type': 'error', 'message': 'init missing coords'});
|
||||||
|
}
|
||||||
|
} else if (type == 'match') {
|
||||||
|
if (flat == null) {
|
||||||
|
sendPort.send({'type': 'error', 'message': 'not inited'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int id = message['id'] as int;
|
||||||
|
final double lat = (message['lat'] as num).toDouble();
|
||||||
|
final double lng = (message['lng'] as num).toDouble();
|
||||||
|
final int lastIndex = (message['lastIndex'] as int?) ?? 0;
|
||||||
|
final int window = (message['window'] as int?) ?? 120;
|
||||||
|
|
||||||
|
final result =
|
||||||
|
_findClosestWindowInternal(flat!, lat, lng, lastIndex, window);
|
||||||
|
sendPort.send({
|
||||||
|
'type': 'matchResult',
|
||||||
|
'id': id,
|
||||||
|
'index': result['index'],
|
||||||
|
'lat': result['lat'],
|
||||||
|
'lng': result['lng'],
|
||||||
|
'dist': result['dist']
|
||||||
|
});
|
||||||
|
} else if (type == 'dispose') {
|
||||||
|
port.close();
|
||||||
|
sendPort.send({'type': 'disposed'});
|
||||||
|
} else {
|
||||||
|
sendPort.send({'type': 'error', 'message': 'unknown message type'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e, st) {
|
||||||
|
sendPort.send(
|
||||||
|
{'type': 'error', 'message': e.toString(), 'stack': st.toString()});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal helper: projection on segments, windowed search.
|
||||||
|
/// Returns Map {index, lat, lng, dist}
|
||||||
|
Map<String, dynamic> _findClosestWindowInternal(
|
||||||
|
Float64List flat, double lat, double lng, int lastIndex, int window) {
|
||||||
|
final int n = flat.length ~/ 2;
|
||||||
|
final int start = max(0, lastIndex - window);
|
||||||
|
final int end = min(n - 1, lastIndex + window);
|
||||||
|
|
||||||
|
double minDist = double.infinity;
|
||||||
|
int bestIdx = lastIndex;
|
||||||
|
double bestLat = flat[lastIndex * 2];
|
||||||
|
double bestLng = flat[lastIndex * 2 + 1];
|
||||||
|
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
final double aLat = flat[i * 2];
|
||||||
|
final double aLng = flat[i * 2 + 1];
|
||||||
|
final double bLat = flat[(i + 1) * 2];
|
||||||
|
final double bLng = flat[(i + 1) * 2 + 1];
|
||||||
|
|
||||||
|
final proj = _closestPointOnSegmentLatLng(lat, lng, aLat, aLng, bLat, bLng);
|
||||||
|
final double d = proj['dist'] as double;
|
||||||
|
if (d < minDist) {
|
||||||
|
minDist = d;
|
||||||
|
bestLat = proj['lat'] as double;
|
||||||
|
bestLng = proj['lng'] as double;
|
||||||
|
// choose overall index: i or i+1 depending on t
|
||||||
|
final double t = proj['t'] as double;
|
||||||
|
bestIdx = i + (t > 0.5 ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {'index': bestIdx, 'lat': bestLat, 'lng': bestLng, 'dist': minDist};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Projection math on geodetic points approximated in degrees (good for short distances).
|
||||||
|
Map<String, dynamic> _closestPointOnSegmentLatLng(
|
||||||
|
double px, double py, double ax, double ay, double bx, double by) {
|
||||||
|
// Here px=lat, py=lng; ax=lat, ay=lng, etc.
|
||||||
|
final double x0 = px;
|
||||||
|
final double y0 = py;
|
||||||
|
final double x1 = ax;
|
||||||
|
final double y1 = ay;
|
||||||
|
final double x2 = bx;
|
||||||
|
final double y2 = by;
|
||||||
|
|
||||||
|
final double dx = x2 - x1;
|
||||||
|
final double dy = y2 - y1;
|
||||||
|
double t = 0.0;
|
||||||
|
final double len2 = dx * dx + dy * dy;
|
||||||
|
if (len2 > 0) {
|
||||||
|
t = ((x0 - x1) * dx + (y0 - y1) * dy) / len2;
|
||||||
|
if (t < 0) t = 0;
|
||||||
|
if (t > 1) t = 1;
|
||||||
|
}
|
||||||
|
final double projX = x1 + t * dx;
|
||||||
|
final double projY = y1 + t * dy;
|
||||||
|
|
||||||
|
final double distMeters = _haversineDistanceMeters(x0, y0, projX, projY);
|
||||||
|
return {'lat': projX, 'lng': projY, 't': t, 'dist': distMeters};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Haversine distance (meters)
|
||||||
|
double _haversineDistanceMeters(
|
||||||
|
double lat1, double lng1, double lat2, double lng2) {
|
||||||
|
final double R = 6371000.0;
|
||||||
|
final double dLat = _deg2rad(lat2 - lat1);
|
||||||
|
final double dLon = _deg2rad(lng2 - lng1);
|
||||||
|
final double a = sin(dLat / 2) * sin(dLat / 2) +
|
||||||
|
cos(_deg2rad(lat1)) * cos(_deg2rad(lat2)) * sin(dLon / 2) * sin(dLon / 2);
|
||||||
|
final double c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||||
|
return R * c;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _deg2rad(double deg) => deg * pi / 180.0;
|
||||||
@@ -14,6 +14,7 @@ import 'package:sefer_driver/controller/functions/crud.dart';
|
|||||||
import 'package:sefer_driver/main.dart';
|
import 'package:sefer_driver/main.dart';
|
||||||
|
|
||||||
import '../../../views/widgets/mydialoug.dart';
|
import '../../../views/widgets/mydialoug.dart';
|
||||||
|
import '../../firebase/notification_service.dart';
|
||||||
|
|
||||||
class CaptainWalletController extends GetxController {
|
class CaptainWalletController extends GetxController {
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
@@ -291,13 +292,23 @@ class CaptainWalletController extends GetxController {
|
|||||||
'paymentMethod': paymentMethod2.toString(),
|
'paymentMethod': paymentMethod2.toString(),
|
||||||
});
|
});
|
||||||
if (res1 != 'failure') {
|
if (res1 != 'failure') {
|
||||||
Get.find<FirebaseMessagesController>().sendNotificationToDriverMAP(
|
// Get.find<FirebaseMessagesController>().sendNotificationToDriverMAP(
|
||||||
'Transfer',
|
// 'Transfer',
|
||||||
'${'You have transfer to your wallet from'.tr}'
|
// '${'You have transfer to your wallet from'.tr}'
|
||||||
|
// '${box.read(BoxName.nameDriver)}',
|
||||||
|
// amountToNewDriverMap[0]['token'].toString(),
|
||||||
|
// [],
|
||||||
|
// 'order1.wav');
|
||||||
|
NotificationService.sendNotification(
|
||||||
|
target: amountToNewDriverMap[0]['token'].toString(),
|
||||||
|
title: 'Transfer'.tr,
|
||||||
|
body: '${'You have transfer to your wallet from'.tr}'
|
||||||
'${box.read(BoxName.nameDriver)}',
|
'${box.read(BoxName.nameDriver)}',
|
||||||
amountToNewDriverMap[0]['token'].toString(),
|
|
||||||
[],
|
isTopic: false, // Important: this is a token
|
||||||
'order1.wav');
|
tone: 'ding',
|
||||||
|
driverList: [], category: 'Transfer',
|
||||||
|
);
|
||||||
await addSeferWallet('payout fee', '5');
|
await addSeferWallet('payout fee', '5');
|
||||||
|
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
|
|
||||||
import 'package:sefer_driver/views/home/on_boarding_page.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
|
||||||
|
import 'package:sefer_driver/views/auth/captin/login_captin.dart';
|
||||||
|
import 'package:sefer_driver/views/home/on_boarding_page.dart';
|
||||||
|
|
||||||
import '../../constant/box_name.dart';
|
import '../../constant/box_name.dart';
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
|
import '../../onbording_page.dart';
|
||||||
import '../../print.dart';
|
import '../../print.dart';
|
||||||
import '../../views/auth/captin/login_captin.dart';
|
import '../functions/encrypt_decrypt.dart';
|
||||||
|
import '../functions/secure_storage.dart';
|
||||||
|
|
||||||
|
// Assuming you have a home page to navigate to after successful login.
|
||||||
|
// If not, you might need to adjust the navigation target.
|
||||||
|
// import 'package:sefer_driver/views/home/home_page.dart';
|
||||||
|
|
||||||
class SplashScreenController extends GetxController
|
class SplashScreenController extends GetxController
|
||||||
with GetTickerProviderStateMixin {
|
with GetTickerProviderStateMixin {
|
||||||
@@ -20,73 +27,116 @@ class SplashScreenController extends GetxController
|
|||||||
|
|
||||||
String packageInfo = '';
|
String packageInfo = '';
|
||||||
|
|
||||||
Future<void> _getPackageInfo() async {
|
|
||||||
final info = await PackageInfo.fromPlatform();
|
|
||||||
packageInfo = info.version;
|
|
||||||
box.write(BoxName.packagInfo, packageInfo);
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
_getPackageInfo();
|
_setupAnimations();
|
||||||
|
_initializeAndNavigate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupAnimations() {
|
||||||
_animationController = AnimationController(
|
_animationController = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: const Duration(milliseconds: 1500), // Reduced duration
|
duration: const Duration(milliseconds: 1500),
|
||||||
)..forward();
|
);
|
||||||
|
|
||||||
animation =
|
animation =
|
||||||
CurvedAnimation(parent: _animationController, curve: Curves.easeOut);
|
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut);
|
||||||
|
_animationController.forward();
|
||||||
startTimer();
|
|
||||||
_startProgressTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startProgressTimer() {
|
/// This is the core function that initializes the app.
|
||||||
|
/// It runs two tasks simultaneously and navigates only when necessary.
|
||||||
|
Future<void> _initializeAndNavigate() async {
|
||||||
|
// Start getting package info, no need to wait for it.
|
||||||
|
_getPackageInfo();
|
||||||
|
|
||||||
|
const minSplashDurationMs = 4000;
|
||||||
|
_animateProgressBar(minSplashDurationMs);
|
||||||
|
|
||||||
|
// Define the two concurrent tasks
|
||||||
|
final minDuration =
|
||||||
|
Future.delayed(const Duration(milliseconds: minSplashDurationMs));
|
||||||
|
final navigationTargetFuture = _getNavigationTarget();
|
||||||
|
|
||||||
|
// Wait for both tasks to complete
|
||||||
|
await Future.wait([minDuration, navigationTargetFuture]);
|
||||||
|
|
||||||
|
// The future now returns a nullable Widget (Widget?)
|
||||||
|
final Widget? targetPage = await navigationTargetFuture;
|
||||||
|
|
||||||
|
// *** FIX: Only navigate if the targetPage is not null. ***
|
||||||
|
// This prevents navigating again if the login function already handled it.
|
||||||
|
if (targetPage != null) {
|
||||||
|
Get.off(() => targetPage,
|
||||||
|
transition: Transition.fadeIn,
|
||||||
|
duration: const Duration(milliseconds: 500));
|
||||||
|
} else {
|
||||||
Log.print(
|
Log.print(
|
||||||
'box.read(BoxName.phoneDriver): ${box.read(BoxName.phoneDriver)}');
|
"Navigation was handled internally by the login process. Splash screen will not navigate.");
|
||||||
Log.print(
|
}
|
||||||
'box.read(BoxName.phoneVerified): ${box.read(BoxName.phoneVerified)}');
|
}
|
||||||
const totalTime = 3000; // 5 seconds in milliseconds
|
|
||||||
const interval = 50; // Update every 50ms
|
/// Animates the progress bar over a given duration.
|
||||||
|
void _animateProgressBar(int totalMilliseconds) {
|
||||||
|
const interval = 50;
|
||||||
int elapsed = 0;
|
int elapsed = 0;
|
||||||
|
_progressTimer?.cancel();
|
||||||
_progressTimer =
|
_progressTimer =
|
||||||
Timer.periodic(const Duration(milliseconds: interval), (timer) async {
|
Timer.periodic(const Duration(milliseconds: interval), (timer) {
|
||||||
elapsed += interval;
|
elapsed += interval;
|
||||||
progress.value = (elapsed / totalTime).clamp(0.0, 1.0);
|
progress.value = (elapsed / totalMilliseconds).clamp(0.0, 1.0);
|
||||||
|
if (elapsed >= totalMilliseconds) {
|
||||||
if (elapsed >= totalTime) {
|
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
// Get.off(SyrianCardAI());
|
|
||||||
box.read(BoxName.onBoarding) == null
|
|
||||||
? Get.off(() => OnBoardingPage())
|
|
||||||
: box.read(BoxName.phoneDriver) != null &&
|
|
||||||
box.read(BoxName.phoneVerified).toString() == '1'
|
|
||||||
? await Get.put(LoginDriverController())
|
|
||||||
.loginWithGoogleCredential(
|
|
||||||
box.read(BoxName.driverID).toString(),
|
|
||||||
box.read(BoxName.emailDriver).toString())
|
|
||||||
: Get.off(() => LoginCaptin());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void startTimer() async {
|
/// Determines the correct page to navigate to, or returns null if navigation
|
||||||
Timer(const Duration(seconds: 5), () async {
|
/// is expected to be handled by an internal process (like login).
|
||||||
// box.read(BoxName.onBoarding) == null
|
Future<Widget?> _getNavigationTarget() async {
|
||||||
// ? Get.off(() => OnBoardingPage())
|
try {
|
||||||
// : box.read(BoxName.email) != null &&
|
// 1) Onboarding
|
||||||
// box.read(BoxName.phone) != null &&
|
final doneOnboarding = box.read(BoxName.onBoarding) == 'yes';
|
||||||
// box.read(BoxName.isVerified) == '1'
|
if (!doneOnboarding) {
|
||||||
// // ? Get.off(() => const MapPagePassenger())
|
// الأفضل: رجّع الواجهة بدل Get.off داخل الدالة
|
||||||
// ? await Get.put(LoginController()).loginUsingCredentials(
|
return OnBoardingPage();
|
||||||
// box.read(BoxName.passengerID).toString(),
|
}
|
||||||
// box.read(BoxName.email).toString(),
|
|
||||||
// )
|
// 2) Login
|
||||||
// : Get.off(() => LoginPage());
|
final isDriverDataAvailable = box.read(BoxName.phoneDriver) != null;
|
||||||
});
|
if (!isDriverDataAvailable) {
|
||||||
|
return LoginCaptin();
|
||||||
|
}
|
||||||
|
|
||||||
|
final loginController = Get.put(LoginDriverController());
|
||||||
|
|
||||||
|
final AppInitializer initializer = AppInitializer();
|
||||||
|
await initializer.initializeApp();
|
||||||
|
await EncryptionHelper.initialize();
|
||||||
|
|
||||||
|
await loginController.loginWithGoogleCredential(
|
||||||
|
box.read(BoxName.driverID).toString(),
|
||||||
|
box.read(BoxName.emailDriver).toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return null; // لأن loginWithGoogleCredential يوجّه
|
||||||
|
} catch (e) {
|
||||||
|
Log.print("Error during navigation logic: $e");
|
||||||
|
return LoginCaptin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _getPackageInfo() async {
|
||||||
|
try {
|
||||||
|
final info = await PackageInfo.fromPlatform();
|
||||||
|
packageInfo = info.version;
|
||||||
|
await box.write(BoxName.packagInfo, packageInfo);
|
||||||
|
update(); // To update any UI element that might be listening
|
||||||
|
} catch (e) {
|
||||||
|
Log.print("Could not get package info: $e");
|
||||||
|
packageInfo = '1.0.0'; // Default value
|
||||||
|
await box.write(BoxName.packagInfo, packageInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -6603,7 +6603,7 @@ const List<Country> countries = [
|
|||||||
code: "SY",
|
code: "SY",
|
||||||
dialCode: "963",
|
dialCode: "963",
|
||||||
minLength: 9,
|
minLength: 9,
|
||||||
maxLength: 9,
|
maxLength: 10,
|
||||||
),
|
),
|
||||||
Country(
|
Country(
|
||||||
name: "Taiwan",
|
name: "Taiwan",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import '../../constant/links.dart';
|
|||||||
import '../../constant/style.dart';
|
import '../../constant/style.dart';
|
||||||
import '../../main.dart';
|
import '../../main.dart';
|
||||||
import '../../views/widgets/elevated_btn.dart';
|
import '../../views/widgets/elevated_btn.dart';
|
||||||
|
import '../firebase/notification_service.dart';
|
||||||
import '../functions/crud.dart';
|
import '../functions/crud.dart';
|
||||||
|
|
||||||
class PassengerNotificationController extends GetxController {
|
class PassengerNotificationController extends GetxController {
|
||||||
@@ -53,8 +54,16 @@ class PassengerNotificationController extends GetxController {
|
|||||||
'title': title,
|
'title': title,
|
||||||
'body': body,
|
'body': body,
|
||||||
});
|
});
|
||||||
FirebaseMessagesController()
|
// FirebaseMessagesController()
|
||||||
.sendNotificationToPassengerToken(title, body, 'token', [], 'ding.wav');
|
// .sendNotificationToPassengerToken(title, body, 'token', [], 'ding.wav');
|
||||||
|
NotificationService.sendNotification(
|
||||||
|
target: 'token'.toString(),
|
||||||
|
title: title.tr,
|
||||||
|
body: body,
|
||||||
|
isTopic: false, // Important: this is a token
|
||||||
|
tone: 'ding',
|
||||||
|
driverList: [], category: title,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -1,184 +1,178 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:math';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:sefer_driver/constant/box_name.dart';
|
|
||||||
import 'package:sefer_driver/controller/functions/location_controller.dart';
|
|
||||||
import 'package:geolocator/geolocator.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
import 'package:just_audio/just_audio.dart'; // لتشغيل صوت عند وصول رحلة
|
||||||
|
|
||||||
|
import '../../constant/box_name.dart';
|
||||||
import '../../constant/links.dart';
|
import '../../constant/links.dart';
|
||||||
import '../../main.dart';
|
import '../../main.dart'; // للوصول لـ box
|
||||||
import '../../print.dart';
|
|
||||||
import '../../views/widgets/mydialoug.dart';
|
|
||||||
import '../functions/crud.dart';
|
import '../functions/crud.dart';
|
||||||
|
import '../functions/location_controller.dart';
|
||||||
|
|
||||||
class RideAvailableController extends GetxController {
|
class RideAvailableController extends GetxController {
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
Map rideAvailableMap = {};
|
|
||||||
late LatLng southwest;
|
|
||||||
late LatLng northeast;
|
|
||||||
|
|
||||||
LatLngBounds calculateBounds(double lat, double lng, double radiusInMeters) {
|
// RxList: أي تغيير هنا سينعكس فوراً على الشاشة
|
||||||
const double earthRadius = 6378137.0; // Earth's radius in meters
|
RxList<Map<String, dynamic>> availableRides = <Map<String, dynamic>>[].obs;
|
||||||
|
|
||||||
double latDelta = (radiusInMeters / earthRadius) * (180 / pi);
|
DateTime? _lastFetchTime;
|
||||||
double lngDelta =
|
static const _cacheDuration = Duration(seconds: 5); // تقليل مدة الكاش قليلاً
|
||||||
(radiusInMeters / (earthRadius * cos(pi * lat / 180))) * (180 / pi);
|
|
||||||
|
|
||||||
double minLat = lat - latDelta;
|
// مشغل الصوت
|
||||||
double maxLat = lat + latDelta;
|
final AudioPlayer _audioPlayer = AudioPlayer();
|
||||||
|
|
||||||
double minLng = lng - lngDelta;
|
@override
|
||||||
double maxLng = lng + lngDelta;
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
|
||||||
// Ensure the latitude is between -90 and 90
|
// 1. جلب القائمة الأولية من السيرفر (HTTP)
|
||||||
minLat = max(-90.0, minLat);
|
getRideAvailable(forceRefresh: true);
|
||||||
maxLat = min(90.0, maxLat);
|
|
||||||
|
|
||||||
// Ensure the longitude is between -180 and 180
|
// 2. تفعيل الاستماع المباشر للتحديثات (Socket)
|
||||||
minLng = (minLng + 180) % 360 - 180;
|
_initSocketListeners();
|
||||||
maxLng = (maxLng + 180) % 360 - 180;
|
|
||||||
|
|
||||||
// Ensure the bounds are in the correct order
|
|
||||||
if (minLng > maxLng) {
|
|
||||||
double temp = minLng;
|
|
||||||
minLng = maxLng;
|
|
||||||
maxLng = temp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return LatLngBounds(
|
@override
|
||||||
southwest: LatLng(minLat, minLng),
|
void onClose() {
|
||||||
northeast: LatLng(maxLat, maxLng),
|
// تنظيف الموارد عند الخروج
|
||||||
);
|
var socket = Get.find<LocationController>().socket;
|
||||||
|
socket?.off('market_new_ride');
|
||||||
|
socket?.off('ride_taken'); // تم توحيد الحدث لـ ride_taken
|
||||||
|
_audioPlayer.dispose();
|
||||||
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
double calculateDistance(String startLocation) {
|
// ========================================================================
|
||||||
List<String> startLocationParts = startLocation.split(',');
|
// 1. جلب الرحلات (HTTP Request) - الطريقة الجديدة (Lat/Lng)
|
||||||
double startLatitude = double.parse(startLocationParts[0]);
|
// ========================================================================
|
||||||
double startLongitude = double.parse(startLocationParts[1]);
|
Future<void> getRideAvailable({bool forceRefresh = false}) async {
|
||||||
|
// منع الطلبات المتكررة السريعة
|
||||||
// Assuming currentLocation is the driver's location
|
if (!forceRefresh &&
|
||||||
double currentLatitude = Get.find<LocationController>().latitude;
|
_lastFetchTime != null &&
|
||||||
double currentLongitude = Get.find<LocationController>().longitude;
|
DateTime.now().difference(_lastFetchTime!) < _cacheDuration) {
|
||||||
|
return;
|
||||||
return Geolocator.distanceBetween(
|
|
||||||
currentLatitude,
|
|
||||||
currentLongitude,
|
|
||||||
startLatitude,
|
|
||||||
startLongitude,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// void sortRidesByDistance() {
|
|
||||||
// rideAvailableMap['message'].sort((a, b) {
|
|
||||||
// double distanceA = calculateDistance(a['start_location']);
|
|
||||||
// double distanceB = calculateDistance(b['start_location']);
|
|
||||||
// return distanceA.compareTo(distanceB);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
getRideAvailable() async {
|
|
||||||
try {
|
try {
|
||||||
|
if (forceRefresh) {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
update();
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
LatLngBounds bounds = calculateBounds(
|
// الحصول على موقع السائق الحالي
|
||||||
Get.find<LocationController>().myLocation!.latitude,
|
final location = Get.find<LocationController>().myLocation;
|
||||||
Get.find<LocationController>().myLocation!.longitude,
|
|
||||||
4000);
|
|
||||||
|
|
||||||
|
// 🔥 التعديل الجوهري: نرسل Lat/Lng فقط بدلاً من Bounds
|
||||||
var payload = {
|
var payload = {
|
||||||
'southwestLat': bounds.southwest.latitude.toString(),
|
'lat': location.latitude.toString(),
|
||||||
'southwestLon': bounds.southwest.longitude.toString(),
|
'lng': location.longitude.toString(),
|
||||||
'northeastLat': bounds.northeast.latitude.toString(),
|
'radius': '50', // نصف القطر بالكيلومتر (كما حددناه في السيرفر)
|
||||||
'northeastLon': bounds.northeast.longitude.toString(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var res =
|
var res =
|
||||||
await CRUD().get(link: AppLink.getRideWaiting, payload: payload);
|
await CRUD().get(link: AppLink.getRideWaiting, payload: payload);
|
||||||
|
|
||||||
|
isLoading = false;
|
||||||
|
_lastFetchTime = DateTime.now();
|
||||||
|
|
||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
rideAvailableMap = jsonDecode(res);
|
final decodedResponse = jsonDecode(res);
|
||||||
isLoading = false;
|
|
||||||
update();
|
if (decodedResponse is Map && decodedResponse['status'] == 'success') {
|
||||||
|
final rides = decodedResponse['message'];
|
||||||
|
if (rides is List) {
|
||||||
|
// تحويل البيانات وتخزينها
|
||||||
|
availableRides.value = List<Map<String, dynamic>>.from(rides);
|
||||||
} else {
|
} else {
|
||||||
HapticFeedback.lightImpact();
|
availableRides.clear();
|
||||||
Get.dialog(
|
|
||||||
CupertinoAlertDialog(
|
|
||||||
title: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
CupertinoIcons.car,
|
|
||||||
size: 44,
|
|
||||||
color: CupertinoColors.systemGrey,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
"No Rides Available".tr,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 17,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
content: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8),
|
|
||||||
child: Text(
|
|
||||||
"Please check back later for available rides.".tr,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: CupertinoColors.systemGrey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
CupertinoDialogAction(
|
|
||||||
onPressed: () {
|
|
||||||
Get.back();
|
|
||||||
Get.back();
|
|
||||||
},
|
|
||||||
child: Text('OK'.tr),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
barrierDismissible: true,
|
|
||||||
transitionCurve: Curves.easeOutBack,
|
|
||||||
transitionDuration: const Duration(milliseconds: 200),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} else {
|
||||||
isLoading = false;
|
availableRides.clear();
|
||||||
update();
|
|
||||||
Get.dialog(
|
|
||||||
CupertinoAlertDialog(
|
|
||||||
title: const Icon(
|
|
||||||
CupertinoIcons.exclamationmark_triangle_fill,
|
|
||||||
color: CupertinoColors.systemRed,
|
|
||||||
size: 44,
|
|
||||||
),
|
|
||||||
content: Text(
|
|
||||||
"Error fetching rides. Please try again.".tr,
|
|
||||||
style: const TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
CupertinoDialogAction(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
child: Text('OK'.tr),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
update(); // تحديث الواجهة
|
||||||
void onInit() {
|
} catch (e) {
|
||||||
getRideAvailable();
|
isLoading = false;
|
||||||
super.onInit();
|
update();
|
||||||
|
print("Error fetching rides: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 2. الاستماع للسوكيت (Real-time Updates) ⚡
|
||||||
|
// ========================================================================
|
||||||
|
void _initSocketListeners() {
|
||||||
|
var locationCtrl = Get.find<LocationController>();
|
||||||
|
var socket = locationCtrl.socket;
|
||||||
|
|
||||||
|
if (socket == null) {
|
||||||
|
print("⚠️ Socket is null in RideAvailableController");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A. عند وصول رحلة جديدة للسوق (market_new_ride)
|
||||||
|
socket.on('market_new_ride', (data) {
|
||||||
|
print("🔔 Socket: New Ride Market: $data");
|
||||||
|
|
||||||
|
if (data != null && data is Map) {
|
||||||
|
// فلترة: هل نوع السيارة يناسبني؟
|
||||||
|
if (_isCarTypeMatch(data['carType'])) {
|
||||||
|
// منع التكرار (إذا كانت الرحلة موجودة مسبقاً)
|
||||||
|
bool exists = availableRides
|
||||||
|
.any((r) => r['id'].toString() == data['id'].toString());
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
// إضافة الرحلة لأعلى القائمة
|
||||||
|
availableRides.insert(0, Map<String, dynamic>.from(data));
|
||||||
|
|
||||||
|
// تشغيل صوت تنبيه (Bling) 🎵
|
||||||
|
_playNotificationSound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// B. عند أخذ رحلة من قبل سائق آخر (ride_taken)
|
||||||
|
// هذا الحدث يصل من acceptRide.php عبر السوكيت
|
||||||
|
socket.on('ride_taken', (data) {
|
||||||
|
print("🗑️ Socket: Ride Taken: $data");
|
||||||
|
if (data != null && data['ride_id'] != null) {
|
||||||
|
// حذف الرحلة من القائمة فوراً
|
||||||
|
availableRides.removeWhere(
|
||||||
|
(r) => r['id'].toString() == data['ride_id'].toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// دالة مساعدة للتحقق من نوع السيارة
|
||||||
|
bool _isCarTypeMatch(String? rideCarType) {
|
||||||
|
if (rideCarType == null) return false;
|
||||||
|
String myDriverType = box.read(BoxName.carTypeOfDriver).toString();
|
||||||
|
|
||||||
|
// منطق التوزيع الهرمي
|
||||||
|
switch (myDriverType) {
|
||||||
|
case 'Comfort':
|
||||||
|
return ['Speed', 'Comfort', 'Fixed Price'].contains(rideCarType);
|
||||||
|
case 'Speed':
|
||||||
|
case 'Scooter':
|
||||||
|
case 'Awfar Car':
|
||||||
|
return rideCarType == myDriverType;
|
||||||
|
case 'Lady':
|
||||||
|
return ['Comfort', 'Speed', 'Lady'].contains(rideCarType);
|
||||||
|
default:
|
||||||
|
return true; // احتياطياً
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// تشغيل صوت التنبيه
|
||||||
|
Future<void> _playNotificationSound() async {
|
||||||
|
try {
|
||||||
|
// تأكد من وجود الملف في assets وإضافته في pubspec.yaml
|
||||||
|
await _audioPlayer.setAsset('assets/audio/notification.mp3');
|
||||||
|
_audioPlayer.play();
|
||||||
|
} catch (e) {
|
||||||
|
// تجاهل الخطأ إذا لم يوجد ملف صوت
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
350
lib/controller/payment/mtn_new/mtn_payment_new_screen.dart
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
// import 'package:http/http.dart' as http;
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:sefer_driver/constant/links.dart'; // افترض وجود هذا الملف
|
||||||
|
import 'package:sefer_driver/controller/functions/crud.dart'; // افترض وجود هذا الملف
|
||||||
|
import '../../../main.dart'; // افترض وجود box هنا
|
||||||
|
import '../../../constant/box_name.dart'; // افترض وجود هذا الملف
|
||||||
|
|
||||||
|
// Service class to handle MTN payment logic
|
||||||
|
class MtnPaymentService {
|
||||||
|
final String _baseUrl =
|
||||||
|
"${AppLink.paymentServer}/ride/mtn_new"; // تأكد من تعديل المسار
|
||||||
|
|
||||||
|
// Function to create a new invoice
|
||||||
|
Future<String?> createInvoice({
|
||||||
|
required String userId,
|
||||||
|
required String userType, // 'driver' or 'passenger'
|
||||||
|
required double amount,
|
||||||
|
required String mtnPhone,
|
||||||
|
}) async {
|
||||||
|
final url = "$_baseUrl/create_mtn_invoice.php";
|
||||||
|
try {
|
||||||
|
final response = await CRUD().postWallet(
|
||||||
|
// استخدام نفس دالة CRUD
|
||||||
|
link: url,
|
||||||
|
payload: {
|
||||||
|
'user_id': userId,
|
||||||
|
'user_type': userType,
|
||||||
|
'amount': amount.toString(),
|
||||||
|
'mtn_phone': mtnPhone,
|
||||||
|
},
|
||||||
|
).timeout(const Duration(seconds: 15));
|
||||||
|
|
||||||
|
if (response != 'failure') {
|
||||||
|
final data = response;
|
||||||
|
if (data['status'] == 'success' && data['invoice_number'] != null) {
|
||||||
|
debugPrint("MTN Invoice created: ${data['invoice_number']}");
|
||||||
|
return data['invoice_number'].toString();
|
||||||
|
} else {
|
||||||
|
debugPrint("Failed to create MTN invoice: ${data['message']}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint("Server error during MTN invoice creation.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Exception during MTN invoice creation: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check invoice status (polling)
|
||||||
|
Future<bool> checkInvoiceStatus(String invoiceNumber) async {
|
||||||
|
// This should point to a new script on your server that checks mtn_invoices table
|
||||||
|
final url = "$_baseUrl/check_mtn_invoice_status.php";
|
||||||
|
try {
|
||||||
|
final response = await CRUD().postWallet(link: url, payload: {
|
||||||
|
'invoice_number': invoiceNumber,
|
||||||
|
}).timeout(const Duration(seconds: 10));
|
||||||
|
|
||||||
|
if (response != 'failure') {
|
||||||
|
final data = response;
|
||||||
|
return data['status'] == 'success' &&
|
||||||
|
data['invoice_status'] == 'completed';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint("Error checking MTN invoice status: $e");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PaymentStatus {
|
||||||
|
creatingInvoice,
|
||||||
|
waitingForPayment,
|
||||||
|
paymentSuccess,
|
||||||
|
paymentTimeout,
|
||||||
|
paymentError
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentScreenMtn extends StatefulWidget {
|
||||||
|
final double amount;
|
||||||
|
// يمكنك إضافة متغير لتحديد هل المستخدم سائق أم راكب
|
||||||
|
final String userType; // 'driver' or 'passenger'
|
||||||
|
|
||||||
|
const PaymentScreenMtn({
|
||||||
|
super.key,
|
||||||
|
required this.amount,
|
||||||
|
required this.userType,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_PaymentScreenMtnState createState() => _PaymentScreenMtnState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PaymentScreenMtnState extends State<PaymentScreenMtn> {
|
||||||
|
final MtnPaymentService _paymentService = MtnPaymentService();
|
||||||
|
Timer? _pollingTimer;
|
||||||
|
PaymentStatus _status = PaymentStatus.creatingInvoice;
|
||||||
|
String? _invoiceNumber;
|
||||||
|
// جلب البيانات من الـ box
|
||||||
|
final String userId =
|
||||||
|
box.read(BoxName.driverID) ?? box.read(BoxName.passengerID);
|
||||||
|
final String phone = box.read(BoxName.phoneWallet);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_createAndPollInvoice();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pollingTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createAndPollInvoice() async {
|
||||||
|
setState(() => _status = PaymentStatus.creatingInvoice);
|
||||||
|
|
||||||
|
final invoiceNumber = await _paymentService.createInvoice(
|
||||||
|
userId: userId,
|
||||||
|
userType: widget.userType,
|
||||||
|
amount: widget.amount,
|
||||||
|
mtnPhone: phone,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (invoiceNumber != null && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_invoiceNumber = invoiceNumber;
|
||||||
|
_status = PaymentStatus.waitingForPayment;
|
||||||
|
});
|
||||||
|
_startPolling(invoiceNumber);
|
||||||
|
} else if (mounted) {
|
||||||
|
setState(() => _status = PaymentStatus.paymentError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startPolling(String invoiceNumber) {
|
||||||
|
const timeoutDuration = Duration(minutes: 15); // زيادة المهلة
|
||||||
|
var elapsed = Duration.zero;
|
||||||
|
|
||||||
|
_pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
|
||||||
|
elapsed += const Duration(seconds: 5);
|
||||||
|
if (elapsed >= timeoutDuration) {
|
||||||
|
timer.cancel();
|
||||||
|
if (mounted) setState(() => _status = PaymentStatus.paymentTimeout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("Polling... Checking MTN invoice: $invoiceNumber");
|
||||||
|
final isCompleted =
|
||||||
|
await _paymentService.checkInvoiceStatus(invoiceNumber);
|
||||||
|
if (isCompleted && mounted) {
|
||||||
|
timer.cancel();
|
||||||
|
setState(() => _status = PaymentStatus.paymentSuccess);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PopScope(
|
||||||
|
canPop: _status != PaymentStatus.waitingForPayment,
|
||||||
|
onPopInvoked: (didPop) async {
|
||||||
|
if (didPop) return;
|
||||||
|
if (_status == PaymentStatus.waitingForPayment) {
|
||||||
|
final shouldPop = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('هل أنت متأكد؟'),
|
||||||
|
content: const Text(
|
||||||
|
'إذا خرجت الآن، قد تفشل عملية الدفع. عليك إتمامها من تطبيق MTN.'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('البقاء')),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
child: const Text('الخروج')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (shouldPop ?? false) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(title: const Text("الدفع عبر MTN Cash")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Center(child: _buildContentByStatus()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContentByStatus() {
|
||||||
|
switch (_status) {
|
||||||
|
case PaymentStatus.creatingInvoice:
|
||||||
|
return const Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Text("جاري إنشاء فاتورة دفع...", style: TextStyle(fontSize: 16)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
case PaymentStatus.waitingForPayment:
|
||||||
|
return _buildWaitingForPaymentUI();
|
||||||
|
case PaymentStatus.paymentSuccess:
|
||||||
|
return _buildSuccessUI();
|
||||||
|
case PaymentStatus.paymentTimeout:
|
||||||
|
case PaymentStatus.paymentError:
|
||||||
|
return _buildErrorUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildWaitingForPaymentUI() {
|
||||||
|
final currencyFormat = NumberFormat.decimalPattern('ar_SY');
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// **مهم**: استبدل هذا المسار بمسار شعار MTN الصحيح في مشروعك
|
||||||
|
Image.asset('assets/images/cashMTN.png', width: 120),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text("تعليمات الدفع", style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
"المبلغ المطلوب: ${currencyFormat.format(widget.amount)} ل.س",
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.titleMedium
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Card(
|
||||||
|
elevation: 1.5,
|
||||||
|
shape:
|
||||||
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_StepTile(number: 1, text: "افتح تطبيق MTN Cash Mobile."),
|
||||||
|
_StepTile(
|
||||||
|
number: 2,
|
||||||
|
text: "اذهب إلى قسم 'دفع الفواتير' أو 'خدمات الدفع'."),
|
||||||
|
_StepTile(
|
||||||
|
number: 3,
|
||||||
|
text: "ابحث عن 'Intaleq App' في قائمة المفوترين."),
|
||||||
|
_StepTile(
|
||||||
|
number: 4,
|
||||||
|
text:
|
||||||
|
"أدخل رقم هاتفك المسجل لدينا للاستعلام عن الفاتورة."),
|
||||||
|
_StepTile(
|
||||||
|
number: 5,
|
||||||
|
text:
|
||||||
|
"ستظهر لك فاتورة بالمبلغ المطلوب. قم بتأكيد الدفع."),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const LinearProgressIndicator(minHeight: 2),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text("بانتظار تأكيد الدفع من MTN...",
|
||||||
|
style: TextStyle(color: Colors.grey.shade700)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const Text("هذه الشاشة ستتحدث تلقائيًا عند اكتمال الدفع",
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
textAlign: TextAlign.center),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSuccessUI() {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.check_circle, color: Colors.green, size: 80),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text("تم الدفع بنجاح!",
|
||||||
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text("تمت إضافة النقاط إلى حسابك.",
|
||||||
|
style: TextStyle(fontSize: 16)),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text("العودة إلى المحفظة"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildErrorUI() {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error, color: Colors.red, size: 80),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
_status == PaymentStatus.paymentTimeout
|
||||||
|
? "انتهى الوقت المحدد للدفع"
|
||||||
|
: "حدث خطأ أثناء إنشاء الفاتورة",
|
||||||
|
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text("يرجى المحاولة مرة أخرى.", style: TextStyle(fontSize: 16)),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _createAndPollInvoice,
|
||||||
|
child: const Text("المحاولة مرة أخرى"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ويدجت مساعد لعرض خطوات التعليمات بشكل أنيق
|
||||||
|
class _StepTile extends StatelessWidget {
|
||||||
|
final int number;
|
||||||
|
final String text;
|
||||||
|
const _StepTile({required this.number, required this.text});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
radius: 14,
|
||||||
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
|
child: Text("$number",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
title: Text(text),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,74 +1,55 @@
|
|||||||
// لإضافة هذه الحزمة، قم بتشغيل الأمر التالي في الـ Terminal
|
|
||||||
// flutter pub add intl
|
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
// تأكد من استيراد الملفات الصحيحة حسب مشروع السائق الخاص بك
|
||||||
import 'package:sefer_driver/constant/links.dart';
|
import 'package:sefer_driver/constant/links.dart';
|
||||||
import 'package:sefer_driver/controller/functions/crud.dart';
|
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||||
|
|
||||||
import '../../../constant/box_name.dart';
|
import '../../../constant/box_name.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
|
// import '../../../print.dart'; // إذا كنت تستخدمه
|
||||||
|
|
||||||
/// خدمة لإدارة عمليات الدفع المتعلقة بنظام الدفع عبر الرسائل القصيرة
|
// --- خدمة الدفع للسائق (نفس المنطق الخاص بالسائق) ---
|
||||||
class PaymentService {
|
class PaymentService {
|
||||||
final String _baseUrl = "${AppLink.seferPaymentServer}/sms_webhook";
|
final String _baseUrl = "${AppLink.paymentServer}/ride/shamcash";
|
||||||
|
|
||||||
Future<String?> createInvoice({
|
Future<String?> createInvoice({required double amount}) async {
|
||||||
required String userPhone,
|
final url = "$_baseUrl/create_invoice_shamcash.php";
|
||||||
required double amount,
|
|
||||||
}) async {
|
|
||||||
final url = "$_baseUrl/create_invoice.php";
|
|
||||||
try {
|
try {
|
||||||
final response = await CRUD().postWallet(
|
final response = await CRUD().postWallet(
|
||||||
link: url,
|
link: url,
|
||||||
payload: {
|
payload: {
|
||||||
'user_phone': userPhone.toString(),
|
'driverID': box.read(BoxName.driverID), // استخدام driverID
|
||||||
'driverID': box.read(BoxName.driverID),
|
|
||||||
'amount': amount.toString(),
|
'amount': amount.toString(),
|
||||||
},
|
},
|
||||||
).timeout(const Duration(seconds: 15)); // إضافة مهلة للطلب
|
).timeout(const Duration(seconds: 15));
|
||||||
|
|
||||||
if (response != 'failure') {
|
if (response != 'failure') {
|
||||||
final data = (response);
|
final data = response;
|
||||||
if (data['status'] == 'success' && data['invoice_number'] != null) {
|
if (data['status'] == 'success' && data['invoice_number'] != null) {
|
||||||
debugPrint(
|
|
||||||
"تم إنشاء الفاتورة بنجاح. الرقم: ${data['invoice_number']}");
|
|
||||||
return data['invoice_number'].toString();
|
return data['invoice_number'].toString();
|
||||||
} else {
|
|
||||||
debugPrint("فشل في إنشاء الفاتورة من السيرفر: ${data['message']}");
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
debugPrint("خطأ في السيرفر عند إنشاء الفاتورة: ${response.statusCode}");
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("حدث استثناء عند إنشاء الفاتورة: $e");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// دالة للتحقق من حالة فاتورة واحدة
|
|
||||||
Future<bool> checkInvoiceStatus(String invoiceNumber) async {
|
Future<bool> checkInvoiceStatus(String invoiceNumber) async {
|
||||||
final url = "$_baseUrl/check_invoice_status.php";
|
final url = "$_baseUrl/check_status.php";
|
||||||
try {
|
try {
|
||||||
final response = await CRUD().postWallet(link: url, payload: {
|
final response = await CRUD().postWallet(link: url, payload: {
|
||||||
'invoice_number': invoiceNumber,
|
'invoice_number': invoiceNumber,
|
||||||
}).timeout(const Duration(seconds: 10)); // مهلة للشبكة
|
}).timeout(const Duration(seconds: 10));
|
||||||
|
|
||||||
if (response != 'failure') {
|
if (response != 'failure') {
|
||||||
final data = (response);
|
final data = response;
|
||||||
return data['status'] == 'success' &&
|
return data['status'] == 'success' &&
|
||||||
data['invoice_status'] == 'completed';
|
data['invoice_status'] == 'completed';
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("خطأ أثناء التحقق من الفاتورة: $e");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,14 +67,14 @@ class PaymentScreenSmsProvider extends StatefulWidget {
|
|||||||
final double amount;
|
final double amount;
|
||||||
final String providerName;
|
final String providerName;
|
||||||
final String providerLogo;
|
final String providerLogo;
|
||||||
final String paymentPhoneNumber;
|
final String qrImagePath;
|
||||||
|
|
||||||
const PaymentScreenSmsProvider({
|
const PaymentScreenSmsProvider({
|
||||||
super.key,
|
super.key,
|
||||||
required this.amount,
|
required this.amount,
|
||||||
this.providerName = 'شام كاش',
|
this.providerName = 'شام كاش',
|
||||||
this.providerLogo = 'assets/images/shamCash.png',
|
this.providerLogo = 'assets/images/shamCash.png',
|
||||||
this.paymentPhoneNumber = '963942542053',
|
this.qrImagePath = 'assets/images/shamcashsend.png',
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -101,32 +82,54 @@ class PaymentScreenSmsProvider extends StatefulWidget {
|
|||||||
_PaymentScreenSmsProviderState();
|
_PaymentScreenSmsProviderState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
final PaymentService _paymentService = PaymentService();
|
final PaymentService _paymentService = PaymentService();
|
||||||
Timer? _pollingTimer;
|
Timer? _pollingTimer;
|
||||||
PaymentStatus _status = PaymentStatus.creatingInvoice;
|
PaymentStatus _status = PaymentStatus.creatingInvoice;
|
||||||
String? _invoiceNumber;
|
String? _invoiceNumber;
|
||||||
final String phone = box.read(BoxName.phoneWallet);
|
|
||||||
|
// العنوان الثابت للدفع (كما في تطبيق الراكب)
|
||||||
|
final String _paymentAddress = "80f23afe40499b02f49966c3340ae0fc";
|
||||||
|
|
||||||
|
// متغيرات الأنيميشن (الوميض)
|
||||||
|
late AnimationController _blinkController;
|
||||||
|
late Animation<Color?> _colorAnimation;
|
||||||
|
late Animation<double> _shadowAnimation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
// إعداد الأنيميشن (وميض أحمر)
|
||||||
|
_blinkController = AnimationController(
|
||||||
|
duration: const Duration(milliseconds: 800),
|
||||||
|
vsync: this,
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
|
||||||
|
_colorAnimation = ColorTween(
|
||||||
|
begin: Colors.red.shade700,
|
||||||
|
end: Colors.red.shade100,
|
||||||
|
).animate(_blinkController);
|
||||||
|
|
||||||
|
_shadowAnimation = Tween<double>(begin: 2.0, end: 15.0).animate(
|
||||||
|
CurvedAnimation(parent: _blinkController, curve: Curves.easeInOut),
|
||||||
|
);
|
||||||
|
|
||||||
_createAndPollInvoice();
|
_createAndPollInvoice();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_pollingTimer?.cancel(); // مهم جداً: إلغاء المؤقت عند الخروج من الشاشة
|
_pollingTimer?.cancel();
|
||||||
|
_blinkController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _createAndPollInvoice() async {
|
void _createAndPollInvoice() async {
|
||||||
setState(() => _status = PaymentStatus.creatingInvoice);
|
setState(() => _status = PaymentStatus.creatingInvoice);
|
||||||
|
final invoiceNumber =
|
||||||
final invoiceNumber = await _paymentService.createInvoice(
|
await _paymentService.createInvoice(amount: widget.amount);
|
||||||
userPhone: phone,
|
|
||||||
amount: widget.amount,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (invoiceNumber != null && mounted) {
|
if (invoiceNumber != null && mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -140,7 +143,7 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _startPolling(String invoiceNumber) {
|
void _startPolling(String invoiceNumber) {
|
||||||
const timeoutDuration = Duration(minutes: 3);
|
const timeoutDuration = Duration(minutes: 5);
|
||||||
var elapsed = Duration.zero;
|
var elapsed = Duration.zero;
|
||||||
|
|
||||||
_pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
|
_pollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
|
||||||
@@ -150,64 +153,57 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
|||||||
if (mounted) setState(() => _status = PaymentStatus.paymentTimeout);
|
if (mounted) setState(() => _status = PaymentStatus.paymentTimeout);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint("Polling... Checking invoice status for: $invoiceNumber");
|
|
||||||
final isCompleted =
|
final isCompleted =
|
||||||
await _paymentService.checkInvoiceStatus(invoiceNumber);
|
await _paymentService.checkInvoiceStatus(invoiceNumber);
|
||||||
if (isCompleted && mounted) {
|
if (isCompleted && mounted) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
setState(() => _status = PaymentStatus.paymentSuccess);
|
setState(() => _status = PaymentStatus.paymentSuccess);
|
||||||
// TODO: تحديث رصيد المستخدم أو تنفيذ الإجراءات اللازمة
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// دالة جديدة لمعالجة محاولة الرجوع للخلف
|
Future<bool> _onPopInvoked() async {
|
||||||
void _onPopInvoked(bool didPop) async {
|
|
||||||
// إذا كان الرجوع قد تم بالفعل (مثلاً من خلال Navigator.pop)، لا تفعل شيئاً
|
|
||||||
if (didPop) return;
|
|
||||||
|
|
||||||
// إذا كان المستخدم ينتظر الدفع، أظهر له حوار التأكيد
|
|
||||||
if (_status == PaymentStatus.waitingForPayment) {
|
if (_status == PaymentStatus.waitingForPayment) {
|
||||||
final shouldPop = await showDialog<bool>(
|
return (await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('هل أنت متأكد؟'),
|
title: const Text('إلغاء العملية؟', textAlign: TextAlign.right),
|
||||||
content: const Text('إذا خرجت الآن، سيتم إلغاء عملية الدفع الحالية.'),
|
content: const Text(
|
||||||
|
'الخروج الآن سيؤدي لإلغاء متابعة عملية الدفع.',
|
||||||
|
textAlign: TextAlign.right),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(false),
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
child: const Text('البقاء'),
|
child: const Text('البقاء')),
|
||||||
),
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(true),
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
child: const Text('الخروج'),
|
child: const Text('خروج',
|
||||||
),
|
style: TextStyle(color: Colors.red))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
)) ??
|
||||||
|
false;
|
||||||
// إذا وافق المستخدم على الخروج، قم بإغلاق الشاشة
|
|
||||||
if (shouldPop ?? false) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// استخدام PopScope بدلاً من WillPopScope
|
return WillPopScope(
|
||||||
return PopScope(
|
onWillPop: _onPopInvoked,
|
||||||
// منع الرجوع التلقائي فقط في حالة انتظار الدفع
|
|
||||||
canPop: _status != PaymentStatus.waitingForPayment,
|
|
||||||
// استدعاء دالة التحقق عند محاولة الرجوع
|
|
||||||
onPopInvoked: _onPopInvoked,
|
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(title: Text("الدفع عبر ${widget.providerName}")),
|
backgroundColor: Colors.grey[50],
|
||||||
body: Padding(
|
appBar: AppBar(
|
||||||
padding: const EdgeInsets.all(16.0),
|
title: Text("دفع عبر ${widget.providerName}"),
|
||||||
child: Center(
|
centerTitle: true,
|
||||||
child: _buildContentByStatus(),
|
elevation: 0,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Center(child: _buildContentByStatus()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -222,7 +218,7 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
|||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(),
|
CircularProgressIndicator(),
|
||||||
SizedBox(height: 20),
|
SizedBox(height: 20),
|
||||||
Text("جاري إنشاء فاتورة الدفع...", style: TextStyle(fontSize: 16)),
|
Text("جاري إنشاء رقم البيان...", style: TextStyle(fontSize: 16)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
case PaymentStatus.waitingForPayment:
|
case PaymentStatus.waitingForPayment:
|
||||||
@@ -237,94 +233,259 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
|||||||
|
|
||||||
Widget _buildWaitingForPaymentUI() {
|
Widget _buildWaitingForPaymentUI() {
|
||||||
final currencyFormat = NumberFormat.decimalPattern('ar_SY');
|
final currencyFormat = NumberFormat.decimalPattern('ar_SY');
|
||||||
final invoiceText = _invoiceNumber ?? '------';
|
final invoiceText = _invoiceNumber ?? '---';
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
|
physics: const BouncingScrollPhysics(),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// 1. المبلغ المطلوب
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [Colors.blue.shade800, Colors.blue.shade600]),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blue.withOpacity(0.25),
|
||||||
|
blurRadius: 15,
|
||||||
|
offset: const Offset(0, 8))
|
||||||
|
],
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Image.asset(widget.providerLogo, width: 96),
|
const Text("المبلغ المطلوب شحنه",
|
||||||
const SizedBox(height: 16),
|
style: TextStyle(color: Colors.white70, fontSize: 14)),
|
||||||
Text("تعليمات الدفع", style: Theme.of(context).textTheme.titleLarge),
|
const SizedBox(height: 5),
|
||||||
const SizedBox(height: 12),
|
Text(
|
||||||
Card(
|
"${currencyFormat.format(widget.amount)} ل.س",
|
||||||
elevation: 1.5,
|
style: const TextStyle(
|
||||||
shape:
|
color: Colors.white,
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
fontSize: 28,
|
||||||
child: Padding(
|
fontWeight: FontWeight.bold),
|
||||||
padding: const EdgeInsets.all(16),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
|
||||||
|
// 2. رقم البيان (الإطار الأحمر الوامض)
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: _blinkController,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: _colorAnimation.value ?? Colors.red,
|
||||||
|
width: 3.0, // إطار سميك
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: (_colorAnimation.value ?? Colors.red)
|
||||||
|
.withOpacity(0.4),
|
||||||
|
blurRadius: _shadowAnimation.value,
|
||||||
|
spreadRadius: 2,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_StepTile(number: 1, text: "افتح تطبيق محفظتك الإلكترونية."),
|
Row(
|
||||||
_StepTile(number: 2, text: "اختر خدمة تحويل الأموال."),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
_StepTile(
|
children: [
|
||||||
number: 3,
|
Icon(Icons.warning_rounded,
|
||||||
text:
|
color: Colors.red.shade800, size: 28),
|
||||||
"أدخل المبلغ المطلوب: ${currencyFormat.format(widget.amount)} ل.س"),
|
const SizedBox(width: 8),
|
||||||
_StepTile(number: 4, text: "حوّل إلى الرقم التالي:"),
|
Text(
|
||||||
// --- التعديل هنا ---
|
"هام جداً: لا تنسَ!",
|
||||||
ListTile(
|
style: TextStyle(
|
||||||
contentPadding: EdgeInsets.zero,
|
color: Colors.red.shade900,
|
||||||
title: Text(
|
|
||||||
widget.paymentPhoneNumber,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
letterSpacing: 1.2),
|
fontSize: 18),
|
||||||
),
|
),
|
||||||
trailing: OutlinedButton.icon(
|
],
|
||||||
onPressed: () async {
|
),
|
||||||
await Clipboard.setData(
|
const SizedBox(height: 10),
|
||||||
ClipboardData(text: widget.paymentPhoneNumber));
|
const Text(
|
||||||
if (mounted) {
|
"يجب نسخ (رقم البيان) هذا ووضعه في تطبيق شام كاش لضمان نجاح العملية.",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: Colors.black87,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: invoiceText));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text("تم نسخ رقم الهاتف")));
|
content: const Text("تم نسخ رقم البيان ✅",
|
||||||
}
|
textAlign: TextAlign.center),
|
||||||
|
backgroundColor: Colors.red.shade700,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
margin: const EdgeInsets.all(20),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.copy, size: 18),
|
borderRadius: BorderRadius.circular(12),
|
||||||
label: const Text("نسخ"),
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 15, vertical: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border:
|
||||||
|
Border.all(color: Colors.red.shade200, width: 1),
|
||||||
),
|
),
|
||||||
),
|
child: Row(
|
||||||
// --- نهاية التعديل ---
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
const SizedBox(height: 8),
|
children: [
|
||||||
_StepTile(
|
Column(
|
||||||
number: 5,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
text: "هام: انسخ رقم القسيمة والصقه في خانة \"البيان\"."),
|
children: [
|
||||||
ListTile(
|
const Text("رقم البيان (Invoice No)",
|
||||||
contentPadding: EdgeInsets.zero,
|
style: TextStyle(
|
||||||
title: Text(invoiceText,
|
fontSize: 12, color: Colors.grey)),
|
||||||
style: const TextStyle(
|
Text(invoiceText,
|
||||||
fontSize: 20,
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
letterSpacing: 1.5)),
|
letterSpacing: 2.0,
|
||||||
trailing: OutlinedButton.icon(
|
color: Colors.red.shade900)),
|
||||||
onPressed: _invoiceNumber == null
|
],
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
await Clipboard.setData(
|
|
||||||
ClipboardData(text: invoiceText));
|
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text("تم نسخ رقم القسيمة")));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.copy, size: 18),
|
|
||||||
label: const Text("نسخ"),
|
|
||||||
),
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.copy_rounded,
|
||||||
|
color: Colors.red.shade900, size: 24),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
|
||||||
|
// 3. عنوان الدفع (للتسهيل على السائق)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text("عنوان الدفع (Payment Address)",
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
InkWell(
|
||||||
|
onTap: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: _paymentAddress));
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: const Text("تم نسخ عنوان الدفع ✅",
|
||||||
|
textAlign: TextAlign.center),
|
||||||
|
backgroundColor: Colors.green.shade600,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
margin: const EdgeInsets.all(20),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_paymentAddress,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontFamily: 'Courier',
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Icon(Icons.copy, size: 18, color: Colors.grey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// 4. الـ QR Code
|
||||||
|
const Text("امسح الرمز للدفع",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87)),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: InteractiveViewer(
|
||||||
|
child: Image.asset(widget.qrImagePath),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(15),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
child: Image.asset(
|
||||||
|
widget.qrImagePath,
|
||||||
|
width: 150,
|
||||||
|
height: 150,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
errorBuilder: (c, o, s) =>
|
||||||
|
const Icon(Icons.qr_code_2, size: 100, color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
// مؤشر الانتظار
|
||||||
|
const LinearProgressIndicator(backgroundColor: Colors.white),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Text("ننتظر إشعار الدفع تلقائياً...",
|
||||||
|
style: TextStyle(color: Colors.grey, fontSize: 12)),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const LinearProgressIndicator(minHeight: 2),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text("بانتظار تأكيد الدفع...",
|
|
||||||
style: TextStyle(color: Colors.grey.shade700)),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
const Text("هذه الشاشة ستتحدث تلقائيًا",
|
|
||||||
style: TextStyle(color: Colors.grey)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -334,14 +495,26 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
|||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.check_circle, color: Colors.green, size: 80),
|
const Icon(Icons.verified_rounded, color: Colors.green, size: 100),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
const Text("تم الدفع بنجاح!",
|
const Text("تم الدفع بنجاح!",
|
||||||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
|
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 10),
|
||||||
ElevatedButton(
|
const Text("تم إضافة الرصيد إلى محفظتك",
|
||||||
|
style: TextStyle(color: Colors.grey)),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12))),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
child: const Text("العودة"),
|
child: const Text("متابعة", style: TextStyle(fontSize: 18)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -351,47 +524,41 @@ class _PaymentScreenSmsProviderState extends State<PaymentScreenSmsProvider> {
|
|||||||
return Column(
|
return Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.error, color: Colors.red, size: 80),
|
Icon(Icons.error_outline_rounded, color: Colors.red.shade400, size: 80),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text(
|
Text(
|
||||||
_status == PaymentStatus.paymentTimeout
|
_status == PaymentStatus.paymentTimeout
|
||||||
? "انتهى الوقت المحدد للدفع"
|
? "انتهى الوقت"
|
||||||
: "حدث خطأ ما",
|
: "لم يتم التحقق",
|
||||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 30),
|
||||||
|
child: Text(
|
||||||
|
"لم يصلنا إشعار الدفع. هل تأكدت من وضع (رقم البيان) في الملاحظات؟",
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.grey, height: 1.5)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 40),
|
||||||
const Text("يرجى المحاولة مرة أخرى.", style: TextStyle(fontSize: 16)),
|
SizedBox(
|
||||||
const SizedBox(height: 20),
|
width: double.infinity,
|
||||||
ElevatedButton(
|
child: ElevatedButton.icon(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12))),
|
||||||
onPressed: _createAndPollInvoice,
|
onPressed: _createAndPollInvoice,
|
||||||
child: const Text("المحاولة مرة أخرى"),
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text("حاول مرة أخرى"),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text("إلغاء", style: TextStyle(color: Colors.grey)),
|
||||||
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ويدجت مساعد لعرض خطوات التعليمات بشكل أنيق
|
|
||||||
class _StepTile extends StatelessWidget {
|
|
||||||
final int number;
|
|
||||||
final String text;
|
|
||||||
const _StepTile({required this.number, required this.text});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
leading: CircleAvatar(
|
|
||||||
radius: 12,
|
|
||||||
backgroundColor: Theme.of(context).primaryColor,
|
|
||||||
child: Text("$number",
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold)),
|
|
||||||
),
|
|
||||||
title: Text(text),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:sefer_driver/constant/box_name.dart';
|
import 'package:sefer_driver/constant/box_name.dart';
|
||||||
import 'package:sefer_driver/constant/colors.dart';
|
|
||||||
import 'package:sefer_driver/constant/links.dart';
|
import 'package:sefer_driver/constant/links.dart';
|
||||||
import 'package:sefer_driver/controller/functions/crud.dart';
|
import 'package:sefer_driver/controller/functions/crud.dart';
|
||||||
import 'package:sefer_driver/controller/functions/encrypt_decrypt.dart';
|
|
||||||
import 'package:sefer_driver/main.dart';
|
import 'package:sefer_driver/main.dart';
|
||||||
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||||
@@ -24,9 +22,10 @@ class RatingController extends GetxController {
|
|||||||
|
|
||||||
void _redirectToAppStore() async {
|
void _redirectToAppStore() async {
|
||||||
// URLs for App Store and Google Play Store
|
// URLs for App Store and Google Play Store
|
||||||
const appStoreUrl = 'https://apps.apple.com/app/6502189302';
|
const appStoreUrl =
|
||||||
|
'https://apps.apple.com/st/app/intaleq-driver/id6482995159';
|
||||||
const playStoreUrl =
|
const playStoreUrl =
|
||||||
'https://play.google.com/store/apps/details?id=com.sefer_driver';
|
'https://play.google.com/store/apps/details?id=com.intaleq_driver';
|
||||||
final url = GetPlatform.isIOS ? appStoreUrl : playStoreUrl;
|
final url = GetPlatform.isIOS ? appStoreUrl : playStoreUrl;
|
||||||
|
|
||||||
if (await launchUrl(Uri.parse(url))) {
|
if (await launchUrl(Uri.parse(url))) {
|
||||||
@@ -59,13 +58,14 @@ class RatingController extends GetxController {
|
|||||||
if (res != 'failure') {
|
if (res != 'failure') {
|
||||||
try {
|
try {
|
||||||
// Attempt to parse the response as JSON
|
// Attempt to parse the response as JSON
|
||||||
final parsedResponse = jsonDecode(res);
|
final parsedResponse = (res);
|
||||||
|
|
||||||
if (parsedResponse['status'] == 'success') {
|
if (parsedResponse['status'] == 'success') {
|
||||||
// Display a success message
|
// Display a success message
|
||||||
CRUD().post(link: AppLink.sendEmailRateingApp, payload: {
|
CRUD().post(link: AppLink.sendEmailRateingApp, payload: {
|
||||||
"name": payload["name"],
|
"name": payload["name"],
|
||||||
"email": payload["email"],
|
"email": payload["email"],
|
||||||
|
"phone": payload["phone"],
|
||||||
"rating": rating.toString(),
|
"rating": rating.toString(),
|
||||||
"comment": payload["comment"],
|
"comment": payload["comment"],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import 'package:sefer_driver/main.dart';
|
|||||||
import 'package:sefer_driver/views/home/Captin/home_captain/home_captin.dart';
|
import 'package:sefer_driver/views/home/Captin/home_captain/home_captin.dart';
|
||||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||||
|
|
||||||
|
import '../firebase/notification_service.dart';
|
||||||
|
import '../home/captin/home_captain_controller.dart';
|
||||||
|
|
||||||
// import '../home/captin/home_captain_controller.dart';
|
// import '../home/captin/home_captain_controller.dart';
|
||||||
|
|
||||||
class RateController extends GetxController {
|
class RateController extends GetxController {
|
||||||
@@ -58,7 +61,7 @@ class RateController extends GetxController {
|
|||||||
var paymentToken3 = await Get.find<MapDriverController>()
|
var paymentToken3 = await Get.find<MapDriverController>()
|
||||||
.generateTokenDriver((-1 * remainingFee).toString());
|
.generateTokenDriver((-1 * remainingFee).toString());
|
||||||
|
|
||||||
await CRUD().post(link: AppLink.addDrivePayment, payload: {
|
await CRUD().postWallet(link: AppLink.addDrivePayment, payload: {
|
||||||
'rideId': 'remain$rideId',
|
'rideId': 'remain$rideId',
|
||||||
'amount': (-1 * remainingFee).toString(),
|
'amount': (-1 * remainingFee).toString(),
|
||||||
'payment_method': 'Remainder',
|
'payment_method': 'Remainder',
|
||||||
@@ -66,13 +69,14 @@ class RateController extends GetxController {
|
|||||||
'token': paymentToken3,
|
'token': paymentToken3,
|
||||||
'driverID': box.read(BoxName.driverID).toString(),
|
'driverID': box.read(BoxName.driverID).toString(),
|
||||||
});
|
});
|
||||||
|
NotificationService.sendNotification(
|
||||||
Get.find<FirebaseMessagesController>().sendNotificationToDriverMAP(
|
target: Get.find<MapDriverController>().tokenPassenger.toString(),
|
||||||
'Wallet Added'.tr,
|
title: 'Wallet Added'.tr,
|
||||||
'Wallet Added${(remainingFee).toStringAsFixed(0)}'.tr,
|
body: 'Wallet Added${(remainingFee).toStringAsFixed(0)}'.tr,
|
||||||
Get.find<MapDriverController>().tokenPassenger,
|
isTopic: false, // Important: this is a token
|
||||||
[],
|
tone: 'tone2',
|
||||||
'tone2.wav');
|
driverList: [], category: 'Wallet Added',
|
||||||
|
);
|
||||||
walletChecked = 'true';
|
walletChecked = 'true';
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@@ -108,24 +112,13 @@ class RateController extends GetxController {
|
|||||||
middleText: '',
|
middleText: '',
|
||||||
confirm: MyElevatedButton(title: 'Ok', onPressed: () => Get.back()));
|
confirm: MyElevatedButton(title: 'Ok', onPressed: () => Get.back()));
|
||||||
} else {
|
} else {
|
||||||
await CRUD().post(
|
await CRUD().post(link: "${AppLink.server}/ride/rate/add.php", payload: {
|
||||||
link: "${AppLink.seferCairoServer}/ride/rate/add.php",
|
|
||||||
payload: {
|
|
||||||
'passenger_id': passengerId,
|
'passenger_id': passengerId,
|
||||||
'driverID': box.read(BoxName.driverID).toString(),
|
'driverID': box.read(BoxName.driverID).toString(),
|
||||||
'rideId': rideId.toString(),
|
'rideId': rideId.toString(),
|
||||||
'rating': selectedRateItemId.toString(),
|
'rating': selectedRateItemId.toString(),
|
||||||
'comment': comment.text ?? 'none',
|
'comment': comment.text ?? 'none',
|
||||||
});
|
});
|
||||||
if (AppLink.endPoint != AppLink.seferCairoServer) {
|
|
||||||
CRUD().post(link: "${AppLink.endPoint}/ride/rate/add.php", payload: {
|
|
||||||
'passenger_id': passengerId,
|
|
||||||
'driverID': box.read(BoxName.driverID).toString(),
|
|
||||||
'rideId': rideId.toString(),
|
|
||||||
'rating': selectedRateItemId.toString(),
|
|
||||||
'comment': comment.text ?? 'none',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
CRUD().sendEmail(AppLink.sendEmailToPassengerForTripDetails, {
|
CRUD().sendEmail(AppLink.sendEmailToPassengerForTripDetails, {
|
||||||
'startLocation':
|
'startLocation':
|
||||||
@@ -143,10 +136,15 @@ class RateController extends GetxController {
|
|||||||
'endNameLocation':
|
'endNameLocation':
|
||||||
Get.find<MapDriverController>().endNameLocation.toString(),
|
Get.find<MapDriverController>().endNameLocation.toString(),
|
||||||
});
|
});
|
||||||
// homeCaptainController.isActive = true;
|
if (Get.isRegistered<MapDriverController>()) {
|
||||||
// update();
|
Get.find<MapDriverController>()
|
||||||
// homeCaptainController.getPaymentToday();
|
.disposeEverything(); // الدالة التي أنشأناها في الخطوة 3
|
||||||
|
Get.delete<MapDriverController>(force: true); // حذف إجباري من الذاكرة
|
||||||
|
}
|
||||||
Get.offAll(HomeCaptain());
|
Get.offAll(HomeCaptain());
|
||||||
|
if (Get.isRegistered<HomeCaptainController>()) {
|
||||||
|
Get.find<HomeCaptainController>().resumeHomeMapUpdates();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
lib/env/env.dart
vendored
@@ -6,6 +6,11 @@ part 'env.g.dart';
|
|||||||
abstract class Env {
|
abstract class Env {
|
||||||
@EnviedField(varName: 'basicAuthCredentials', obfuscate: true)
|
@EnviedField(varName: 'basicAuthCredentials', obfuscate: true)
|
||||||
static final String basicAuthCredentials = _Env.basicAuthCredentials;
|
static final String basicAuthCredentials = _Env.basicAuthCredentials;
|
||||||
|
@EnviedField(varName: 'mapKeyOsm', obfuscate: true)
|
||||||
|
static final String mapKeyOsm = _Env.mapKeyOsm;
|
||||||
|
|
||||||
|
@EnviedField(varName: 'mapAPIKEYIOS', obfuscate: true)
|
||||||
|
static final String mapAPIKEYIOS = _Env.mapAPIKEYIOS;
|
||||||
|
|
||||||
@EnviedField(varName: 'email', obfuscate: true)
|
@EnviedField(varName: 'email', obfuscate: true)
|
||||||
static final String email = _Env.email;
|
static final String email = _Env.email;
|
||||||
|
|||||||
24562
lib/env/env.g.dart
vendored
350
lib/main.dart
@@ -1,34 +1,38 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:sefer_driver/views/home/Captin/orderCaptin/order_request_page.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:flutter_stripe/flutter_stripe.dart';
|
import 'package:flutter_stripe/flutter_stripe.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:permission_handler/permission_handler.dart'; // ✅ جديد
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:device_info_plus/device_info_plus.dart'; // ✅ جديد
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
||||||
import 'constant/api_key.dart';
|
import 'constant/api_key.dart';
|
||||||
import 'constant/info.dart';
|
import 'constant/info.dart';
|
||||||
|
import 'constant/notification.dart';
|
||||||
import 'controller/firebase/firbase_messge.dart';
|
import 'controller/firebase/firbase_messge.dart';
|
||||||
import 'controller/firebase/local_notification.dart';
|
import 'controller/firebase/local_notification.dart';
|
||||||
import 'controller/functions/add_error.dart';
|
import 'controller/functions/background_service.dart';
|
||||||
import 'controller/functions/battery_status.dart';
|
|
||||||
import 'controller/functions/crud.dart';
|
import 'controller/functions/crud.dart';
|
||||||
import 'controller/functions/encrypt_decrypt.dart';
|
|
||||||
import 'controller/functions/secure_storage.dart';
|
|
||||||
import 'controller/local/local_controller.dart';
|
import 'controller/local/local_controller.dart';
|
||||||
import 'controller/local/translations.dart';
|
import 'controller/local/translations.dart';
|
||||||
import 'firebase_options.dart';
|
import 'firebase_options.dart';
|
||||||
import 'models/db_sql.dart';
|
import 'models/db_sql.dart';
|
||||||
import 'print.dart';
|
import 'print.dart';
|
||||||
import 'splash_screen_page.dart';
|
import 'splash_screen_page.dart';
|
||||||
|
import 'views/home/Captin/orderCaptin/order_request_page.dart';
|
||||||
import 'views/home/Captin/driver_map_page.dart';
|
import 'views/home/Captin/driver_map_page.dart';
|
||||||
import 'views/home/Captin/orderCaptin/order_over_lay.dart';
|
import 'views/home/Captin/orderCaptin/order_over_lay.dart';
|
||||||
|
|
||||||
@@ -36,69 +40,218 @@ final box = GetStorage();
|
|||||||
const storage = FlutterSecureStorage();
|
const storage = FlutterSecureStorage();
|
||||||
DbSql sql = DbSql.instance;
|
DbSql sql = DbSql.instance;
|
||||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
const platform = MethodChannel('com.example.intaleq_driver/app_control');
|
const platform = MethodChannel('com.intaleq_driver/app_control');
|
||||||
|
|
||||||
|
// ✅ قنوات الإشعارات المطلوبة
|
||||||
|
const String backgroundServiceChannelId = 'driver_service_channel';
|
||||||
|
const String locationServiceChannelId = 'location_service_channel';
|
||||||
|
const String geolocatorChannelId = 'geolocator_channel';
|
||||||
|
|
||||||
|
/// تهيئة Firebase بوعي لمنع تهيئة مكرّرة على أندرويد (isolates متعددة)
|
||||||
|
Future<void> initFirebaseIfNeeded() async {
|
||||||
|
if (Firebase.apps.isEmpty) {
|
||||||
|
await Firebase.initializeApp(
|
||||||
|
options: DefaultFirebaseOptions.currentPlatform);
|
||||||
|
} else {
|
||||||
|
Firebase.app();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ طلب إذن الإشعارات على Android 13+
|
||||||
|
Future<bool> requestNotificationPermission() async {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
try {
|
||||||
|
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||||
|
|
||||||
|
if (androidInfo.version.sdkInt >= 33) {
|
||||||
|
final status = await Permission.notification.request();
|
||||||
|
|
||||||
|
if (status.isGranted) {
|
||||||
|
print('✅ Notification permission granted');
|
||||||
|
return true;
|
||||||
|
} else if (status.isDenied) {
|
||||||
|
print('⚠️ Notification permission denied');
|
||||||
|
return false;
|
||||||
|
} else if (status.isPermanentlyDenied) {
|
||||||
|
print('⚠️ Notification permission permanently denied');
|
||||||
|
await openAppSettings();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print('✅ Android < 13, no runtime notification permission needed');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Error requesting notification permission: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true; // iOS
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ إنشاء جميع قنوات الإشعارات المطلوبة
|
||||||
|
Future<void> createAllNotificationChannels() async {
|
||||||
|
if (!Platform.isAndroid) return;
|
||||||
|
|
||||||
|
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
||||||
|
FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
// قناة Background Service
|
||||||
|
const AndroidNotificationChannel backgroundChannel =
|
||||||
|
AndroidNotificationChannel(
|
||||||
|
backgroundServiceChannelId,
|
||||||
|
'خدمة السائق',
|
||||||
|
description: 'استقبال الطلبات في الخلفية',
|
||||||
|
importance: Importance.low,
|
||||||
|
playSound: false,
|
||||||
|
enableVibration: false,
|
||||||
|
showBadge: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// قناة Location Service
|
||||||
|
const AndroidNotificationChannel locationChannel = AndroidNotificationChannel(
|
||||||
|
locationServiceChannelId,
|
||||||
|
'خدمة الموقع',
|
||||||
|
description: 'تتبع موقع السائق',
|
||||||
|
importance: Importance.low,
|
||||||
|
playSound: false,
|
||||||
|
enableVibration: false,
|
||||||
|
showBadge: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// قناة Geolocator
|
||||||
|
const AndroidNotificationChannel geolocatorChannel =
|
||||||
|
AndroidNotificationChannel(
|
||||||
|
geolocatorChannelId,
|
||||||
|
'تحديد الموقع',
|
||||||
|
description: 'خدمة تحديد الموقع الجغرافي',
|
||||||
|
importance: Importance.low,
|
||||||
|
playSound: false,
|
||||||
|
enableVibration: false,
|
||||||
|
showBadge: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// إنشاء جميع القنوات
|
||||||
|
await flutterLocalNotificationsPlugin
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
|
?.createNotificationChannel(backgroundChannel);
|
||||||
|
|
||||||
|
await flutterLocalNotificationsPlugin
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
|
?.createNotificationChannel(locationChannel);
|
||||||
|
|
||||||
|
await flutterLocalNotificationsPlugin
|
||||||
|
.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>()
|
||||||
|
?.createNotificationChannel(geolocatorChannel);
|
||||||
|
|
||||||
|
print('✅ All notification channels created successfully');
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Error creating notification channels: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ============ Handlers: Background ============
|
||||||
|
// في ملف main.dart (خارج كلاس MyApp)
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
Future<void> backgroundMessageHandler(RemoteMessage message) async {
|
Future<void> backgroundMessageHandler(RemoteMessage message) async {
|
||||||
|
// 1. تهيئة بيئة فلاتر في الخلفية
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
|
||||||
await GetStorage.init();
|
|
||||||
|
|
||||||
if (!Get.isRegistered<NotificationController>()) {
|
// 2. تهيئة الكونترولر (لأنه isolate منفصل)
|
||||||
Get.put(NotificationController());
|
// ملاحظة: تأكد أنك لا تعتمد على Context هنا
|
||||||
}
|
final NotificationController notificationController =
|
||||||
if (!Get.isRegistered<FirebaseMessagesController>()) {
|
NotificationController();
|
||||||
Get.put(FirebaseMessagesController());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await FlutterOverlayWindow.isPermissionGranted()) {
|
// مهم جداً: إعادة تهيئة الإشعارات داخل هذه العملية المنفصلة
|
||||||
Log.print("Overlay permission not granted; showing only notification.");
|
await notificationController.initNotifications();
|
||||||
}
|
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
print("🟢 Background Message Received: ${message.data}");
|
||||||
if (message.notification != null && message.notification!.title != null) {
|
|
||||||
Log.print('message.notification!.title: ${message.notification!.title}');
|
|
||||||
|
|
||||||
if (message.notification?.title == 'Order' ||
|
// 3. استخراج البيانات (الآن العنوان والنص داخل data وليس notification)
|
||||||
message.notification?.title == 'OrderSpeed') {
|
String? title = message.data['title'];
|
||||||
var myListString = message.data['DriverList'] ?? '[]';
|
String? body = message.data['body'];
|
||||||
Log.print('myListString: $myListString');
|
String? tone = message.data['tone'] ?? 'order';
|
||||||
|
String? myListString = message.data['DriverList'];
|
||||||
|
|
||||||
List<dynamic> myList;
|
// 4. شرط الأمان: التأكد من وجود البيانات المطلوبة
|
||||||
try {
|
if (title != null && body != null && myListString != null) {
|
||||||
myList = jsonDecode(myListString) as List<dynamic>;
|
// 5. عرض الإشعار المحلي
|
||||||
} catch (e) {
|
notificationController.showOrderNotification(
|
||||||
Log.print('Error decoding JSON: $e');
|
title,
|
||||||
myList = [];
|
body,
|
||||||
}
|
'ding.wav',
|
||||||
|
|
||||||
bool isOverlayActive = await FlutterOverlayWindow.isActive();
|
|
||||||
if (isOverlayActive) {
|
|
||||||
await FlutterOverlayWindow.shareData(myList);
|
|
||||||
} else {
|
|
||||||
await FlutterOverlayWindow.showOverlay(
|
|
||||||
enableDrag: true,
|
|
||||||
flag: OverlayFlag.focusPointer,
|
|
||||||
positionGravity: PositionGravity.auto,
|
|
||||||
height: 1400,
|
|
||||||
width: WindowSize.matchParent,
|
|
||||||
startPosition: const OverlayPosition(0, -30),
|
|
||||||
);
|
|
||||||
await FlutterOverlayWindow.shareData(myList);
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationController().showNotification(
|
|
||||||
message.notification!.title.toString(),
|
|
||||||
message.notification!.body.toString(),
|
|
||||||
'order',
|
|
||||||
myListString,
|
myListString,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
FirebaseMessagesController().fireBaseTitles(message);
|
print("⚠️ Received empty data message or missing fields.");
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// @pragma('vm:entry-point')
|
||||||
|
// Future<void> backgroundMessageHandler(RemoteMessage message) async {
|
||||||
|
// WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
// await initFirebaseIfNeeded();
|
||||||
|
// await GetStorage.init();
|
||||||
|
|
||||||
|
// if (!Get.isRegistered<NotificationController>()) {
|
||||||
|
// Get.put(NotificationController());
|
||||||
|
// }
|
||||||
|
// if (!Get.isRegistered<FirebaseMessagesController>()) {
|
||||||
|
// Get.put(FirebaseMessagesController());
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!await FlutterOverlayWindow.isPermissionGranted()) {
|
||||||
|
// Log.print("Overlay permission not granted; showing only notification.");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (Platform.isAndroid) {
|
||||||
|
// String category = message.data['category'] ?? '';
|
||||||
|
// Log.print('category: ${category}');
|
||||||
|
// // if (message.notification != null) {
|
||||||
|
// // Log.print('message.notification!.title: ${message.notification!.title}');
|
||||||
|
|
||||||
|
// if (category == 'Order' || category == 'OrderSpeed') {
|
||||||
|
// final myListString = message.data['DriverList'] ?? '[]';
|
||||||
|
// Log.print('myListString: $myListString');
|
||||||
|
|
||||||
|
// List<dynamic> myList;
|
||||||
|
// try {
|
||||||
|
// myList = jsonDecode(myListString) as List<dynamic>;
|
||||||
|
// } catch (e) {
|
||||||
|
// Log.print('Error decoding JSON: $e');
|
||||||
|
// myList = [];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // final isOverlayActive = await FlutterOverlayWindow.isActive();
|
||||||
|
// // if (isOverlayActive) {
|
||||||
|
// // await FlutterOverlayWindow.shareData(myList);
|
||||||
|
// // } else {
|
||||||
|
// // await FlutterOverlayWindow.showOverlay(
|
||||||
|
// // enableDrag: true,
|
||||||
|
// // flag: OverlayFlag.focusPointer,
|
||||||
|
// // positionGravity: PositionGravity.auto,
|
||||||
|
// // height: WindowSize.matchParent,
|
||||||
|
// // width: WindowSize.matchParent,
|
||||||
|
// // startPosition: const OverlayPosition(0, -30),
|
||||||
|
// // );
|
||||||
|
// // await FlutterOverlayWindow.shareData(myList);
|
||||||
|
// // }
|
||||||
|
// NotificationController().showOrderNotification(
|
||||||
|
// message.notification?.title ?? "طلب جديد", // العنوان
|
||||||
|
// message.notification?.body ?? "لديك طلب توصيل جديد", // النص الأساسي
|
||||||
|
// 'ding', // اسم نغمة الإشعار (تأكد أنها موجودة في raw)
|
||||||
|
// myListString, // البيانات القادمة من السيرفر (JSON String List)
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// FirebaseMessagesController().fireBaseTitles(message);
|
||||||
|
// }
|
||||||
|
// // }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void notificationTapBackground(NotificationResponse notificationResponse) {
|
void notificationTapBackground(NotificationResponse notificationResponse) {
|
||||||
@@ -106,13 +259,12 @@ void notificationTapBackground(NotificationResponse notificationResponse) {
|
|||||||
NotificationController().handleNotificationResponse(notificationResponse);
|
NotificationController().handleNotificationResponse(notificationResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ============ Entrypoint: Overlay ============
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void overlayMain() async {
|
void overlayMain() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
await GetStorage.init();
|
await GetStorage.init();
|
||||||
await Firebase.initializeApp(
|
|
||||||
options: DefaultFirebaseOptions.currentPlatform,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!Get.isRegistered<NotificationController>()) {
|
if (!Get.isRegistered<NotificationController>()) {
|
||||||
Get.put(NotificationController());
|
Get.put(NotificationController());
|
||||||
@@ -124,48 +276,65 @@ void overlayMain() async {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// إغلاق الـ Overlay عند الحاجة
|
||||||
Future<void> closeOverLay() async {
|
Future<void> closeOverLay() async {
|
||||||
bool isOverlayActive = await FlutterOverlayWindow.isActive();
|
final isOverlayActive = await FlutterOverlayWindow.isActive();
|
||||||
if (isOverlayActive) {
|
if (isOverlayActive) {
|
||||||
await FlutterOverlayWindow.closeOverlay();
|
await FlutterOverlayWindow.closeOverlay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() async {
|
/// ============ Entrypoint: App ============
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
|
||||||
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
|
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runZonedGuarded(() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
await initFirebaseIfNeeded();
|
||||||
await WakelockPlus.enable();
|
await WakelockPlus.enable();
|
||||||
await GetStorage.init();
|
await GetStorage.init();
|
||||||
await initializeDateFormatting();
|
await initializeDateFormatting();
|
||||||
|
|
||||||
Stripe.publishableKey = AK.publishableKeyStripe;
|
Stripe.publishableKey = AK.publishableKeyStripe;
|
||||||
SystemChrome.setPreferredOrientations([
|
|
||||||
|
await SystemChrome.setPreferredOrientations([
|
||||||
DeviceOrientation.portraitUp,
|
DeviceOrientation.portraitUp,
|
||||||
DeviceOrientation.portraitDown,
|
DeviceOrientation.portraitDown,
|
||||||
]);
|
]);
|
||||||
runZonedGuarded<Future<void>>(() async {
|
|
||||||
|
// ✅ الترتيب الصحيح: الإذونات → القنوات → الخدمات
|
||||||
|
|
||||||
|
// 1. طلب إذن الإشعارات أولاً (Android 13+)
|
||||||
|
bool notificationPermissionGranted = await requestNotificationPermission();
|
||||||
|
if (!notificationPermissionGranted) {
|
||||||
|
print('⚠️ تحذير: لم يتم منح إذن الإشعارات - قد لا تعمل بعض الميزات');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. إنشاء جميع قنوات الإشعارات
|
||||||
|
await createAllNotificationChannels();
|
||||||
|
|
||||||
|
// 3. تهيئة الخدمة (بدون تشغيلها)
|
||||||
|
await BackgroundServiceHelper.initialize();
|
||||||
|
|
||||||
|
// 4. سجل الهاندلر تبع رسائل الخلفية
|
||||||
|
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
|
||||||
|
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
}, (error, stack) {
|
}, (error, stack) {
|
||||||
// ==== START: ERROR FILTER ====
|
final errorString = error.toString();
|
||||||
String errorString = error.toString();
|
|
||||||
|
|
||||||
// Print all errors to the local debug console for development
|
|
||||||
print("Caught Dart error: $error");
|
print("Caught Dart error: $error");
|
||||||
print(stack);
|
print(stack);
|
||||||
|
|
||||||
// We will check if the error contains keywords for errors we want to ignore.
|
final isIgnoredError = errorString.contains('PERMISSION_DENIED') ||
|
||||||
// If it's one of them, we will NOT send it to the server.
|
|
||||||
bool isIgnoredError = errorString.contains('PERMISSION_DENIED') ||
|
|
||||||
errorString.contains('FormatException') ||
|
errorString.contains('FormatException') ||
|
||||||
errorString.contains('Null check operator used on a null value');
|
errorString.contains('Null check operator used on a null value');
|
||||||
|
|
||||||
if (!isIgnoredError) {
|
if (!isIgnoredError) {
|
||||||
// Only send the error to the server if it's not in our ignore list.
|
|
||||||
CRUD.addError(error.toString(), stack.toString(), 'main');
|
CRUD.addError(error.toString(), stack.toString(), 'main');
|
||||||
} else {
|
} else {
|
||||||
print("Ignoring error and not sending to server: $errorString");
|
print("Ignoring error and not sending to server: $errorString");
|
||||||
}
|
}
|
||||||
// ==== END: ERROR FILTER ====
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,27 +355,35 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initApp() async {
|
Future<void> _initApp() async {
|
||||||
try {
|
try {
|
||||||
final AppInitializer initializer = AppInitializer();
|
|
||||||
await initializer.initializeApp();
|
|
||||||
await EncryptionHelper.initialize();
|
|
||||||
|
|
||||||
if (!Get.isRegistered<NotificationController>()) {
|
if (!Get.isRegistered<NotificationController>()) {
|
||||||
Get.put(NotificationController());
|
Get.put(NotificationController());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Get.isRegistered<FirebaseMessagesController>()) {
|
if (!Get.isRegistered<FirebaseMessagesController>()) {
|
||||||
Get.put(FirebaseMessagesController());
|
Get.put(FirebaseMessagesController()).getToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
await FirebaseMessaging.instance.requestPermission();
|
await FirebaseMessaging.instance.requestPermission();
|
||||||
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
|
|
||||||
await FirebaseMessagesController().getToken();
|
|
||||||
await NotificationController().initNotifications();
|
await NotificationController().initNotifications();
|
||||||
|
|
||||||
|
// Generate a random index to pick a message
|
||||||
|
final random = Random();
|
||||||
|
final randomMessage =
|
||||||
|
syrianDriverMessages[random.nextInt(syrianDriverMessages.length)];
|
||||||
|
|
||||||
|
// Schedule the notification with the random message
|
||||||
|
NotificationController().scheduleNotificationsForSevenDays(
|
||||||
|
randomMessage.split(':')[0],
|
||||||
|
randomMessage.split(':')[1],
|
||||||
|
"tone1",
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Log.print("Error during _initApp: $e");
|
Log.print("Error during _initApp: $e");
|
||||||
}
|
}
|
||||||
@@ -214,7 +391,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
LocaleController localController = Get.put(LocaleController());
|
final LocaleController localController = Get.put(LocaleController());
|
||||||
return GetMaterialApp(
|
return GetMaterialApp(
|
||||||
navigatorKey: navigatorKey,
|
navigatorKey: navigatorKey,
|
||||||
title: AppInformation.appName,
|
title: AppInformation.appName,
|
||||||
@@ -225,10 +402,11 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
|||||||
initialRoute: '/',
|
initialRoute: '/',
|
||||||
getPages: [
|
getPages: [
|
||||||
GetPage(name: '/', page: () => SplashScreen()),
|
GetPage(name: '/', page: () => SplashScreen()),
|
||||||
GetPage(name: '/order-page', page: () => OrderRequestPage()),
|
GetPage(name: '/OrderRequestPage', page: () => OrderRequestPage()),
|
||||||
GetPage(
|
GetPage(
|
||||||
name: '/passenger-location-map',
|
name: '/passenger-location-map',
|
||||||
page: () => PassengerLocationMapPage()),
|
page: () => PassengerLocationMapPage(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class OverlayMethodChannel {
|
class OverlayMethodChannel {
|
||||||
static const _channel = MethodChannel('com.sefer_driver/app_control');
|
// 1. تم تصحيح اسم القناة ليتطابق مع ملف MainActivity.kt [1]
|
||||||
|
static const _channel = MethodChannel('com.intaleq_driver/app_control');
|
||||||
|
|
||||||
static Future<void> bringToForeground() async {
|
static Future bringToForeground() async {
|
||||||
try {
|
try {
|
||||||
|
// استدعاء الدالة المعرفة في الكوتلن [3]
|
||||||
await _channel.invokeMethod('bringToForeground');
|
await _channel.invokeMethod('bringToForeground');
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
print('Error bringing app to foreground: $e');
|
print('Error bringing app to foreground: $e');
|
||||||
|
|||||||
133
lib/readme.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
إليك التوثيق التقني العميق (Deep Technical Documentation) لتطبيق السائق (Driver App) في منصة **انطلق (Intaleq)**. تم إعداد هذا التقرير بناءً على تحليل الكود المصدري، مع التركيز على البنية التحتية، تدفق البيانات، والخدمات الخلفية.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📘 Intaleq Driver App - Technical Documentation
|
||||||
|
|
||||||
|
**الإصدار:** 1.0
|
||||||
|
**المعمارية:** MVC using GetX
|
||||||
|
**اللغة:** Dart (Flutter)
|
||||||
|
**إدارة الحالة:** GetX (Reactive State Management)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 🔐 التسجيل والتحقق (Onboarding & KYC)
|
||||||
|
|
||||||
|
نظام التسجيل في تطبيق السائق معقد لأنه يتطلب التحقق من الهوية والأهلية قبل السماح للسائق بالعمل.
|
||||||
|
|
||||||
|
### أ. دورة تسجيل السائق (`Driver Registration Flow`)
|
||||||
|
|
||||||
|
- **المسار:** `lib/controller/auth/captin/register_captin_controller.dart`
|
||||||
|
- **الآلية:**
|
||||||
|
1. **التحقق من الهاتف:** يتم استخدام `SmsEgyptController` أو `PhoneAuthHelper` لإرسال OTP. يتم التحقق مما إذا كان الرقم مسجلاً مسبقاً عبر `checkPhoneNumberISVerfiedDriver`.
|
||||||
|
2. **إدخال البيانات الأساسية:** (الاسم، البريد، تاريخ الميلاد) يتم جمعها في `RegistrationView`.
|
||||||
|
3. **إنشاء الحساب:** يتم استدعاء `signUpCaptin.php` لإنشاء سجل أولي للسائق بحالة `yet` (بانتظار التوثيق).
|
||||||
|
|
||||||
|
### ب. نظام رفع الوثائق والمعالجة بالذكاء الاصطناعي (AI & OCR)
|
||||||
|
|
||||||
|
- **المسؤوليـة:** `lib/controller/functions/gemeni.dart` (الكلاس `AI`).
|
||||||
|
- **العملية التقنية:**
|
||||||
|
1. **التقاط الصورة:** يتم استخدام `ImagePicker` و `ImageCropper` لضمان جودة الصورة.
|
||||||
|
2. **الضغط:** يتم ضغط الصورة باستخدام `FlutterImageCompress` لتقليل استهلاك البيانات.
|
||||||
|
3. **التحليل (AI Extraction):**
|
||||||
|
- يتم رفع الصورة إلى `uploadImageToAi`.
|
||||||
|
- يتم تمرير `prompt` هندسي دقيق لنموذج الذكاء الاصطناعي (مثل Gemini أو نماذج OCR مخصصة) لاستخراج البيانات كـ JSON (مثل: `vin`, `make`, `model`, `plate_number`).
|
||||||
|
4. **التعبئة التلقائية:** البيانات المستخرجة تُعبأ تلقائياً في `RegisterCaptainController` ليقوم السائق بمراجعتها فقط، مما يقلل أخطاء الإدخال اليدوي.
|
||||||
|
|
||||||
|
### ج. حالة "بانتظار الموافقة" (`Pending Approval`)
|
||||||
|
|
||||||
|
- **المسؤوليـة:** `LoginDriverController` و `DriverVerificationScreen`.
|
||||||
|
- **المنطق:**
|
||||||
|
- عند تسجيل الدخول، يتحقق النظام من الحقل `status` و `is_verified` في استجابة الـ login.
|
||||||
|
- إذا كانت الحالة `yet`، يتم توجيه السائق إجبارياً إلى شاشة `DriverVerificationScreen` التي تعرض رسالة "Your Application is Under Review" وتمنع الوصول للخريطة.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 📡 الخدمات الخلفية والتتبع (Background Services & Tracking)
|
||||||
|
|
||||||
|
يعتمد التطبيق على نظام تتبع ذكي (Batch Tracking) لتقليل استهلاك البطارية والبيانات مع الحفاظ على الدقة.
|
||||||
|
|
||||||
|
### أ. محرك الموقع (`LocationController`)
|
||||||
|
|
||||||
|
- **المسار:** `lib/controller/functions/location_controller.dart`.
|
||||||
|
- **آلية العمل في الخلفية:**
|
||||||
|
1. **التهيئة:** يتم تفعيل `location.enableBackgroundMode(enable: true)` لضمان استمرار الخدمة عند إغلاق الشاشة.
|
||||||
|
2. **التخزين المؤقت (Buffering):** بدلاً من إرسال كل نقطة GPS للسيرفر، يتم تخزين النقاط في قائمة محلية `_trackBuffer`.
|
||||||
|
3. **التصفية الذكية (Smart Filtering):** لا يتم تسجيل النقطة إلا إذا تحرك السائق مسافة معينة (`onMoveMetersNormal = 15m`) أو مرّ وقت محدد (`30 ثانية`) لضمان تسجيل التوقفات.
|
||||||
|
|
||||||
|
### ب. إرسال البيانات (Batch Upload)
|
||||||
|
|
||||||
|
- يوجد مؤقت `_uploadTimer` يعمل كل **دقيقتين** (في الوضع العادي).
|
||||||
|
- يقوم هذا المؤقت بجمع كل النقاط في `_trackBuffer`، تحويلها لـ JSON، وإرسالها بطلب واحد `POST` إلى `add_batch.php`. هذا يقلل الحمل على السيرفر بنسبة كبيرة جداً.
|
||||||
|
|
||||||
|
### ج. وضع توفير الطاقة (Power Saving Mode)
|
||||||
|
|
||||||
|
- يراقب التطبيق حالة البطارية عبر `BatteryNotifier`. إذا انخفضت عن 20%، يتم تقليل معدل التحديث (تسجيل كل 6 ثواني ورفع كل 4 دقائق).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 🔔 استقبال الطلبات (Request Handling)
|
||||||
|
|
||||||
|
### أ. النافذة العائمة (`Overlay Window`)
|
||||||
|
|
||||||
|
- **التقنية:** `flutter_overlay_window`.
|
||||||
|
- **المسار:** `lib/main.dart` (Background Handler) و `lib/views/home/Captin/orderCaptin/order_over_lay.dart`.
|
||||||
|
- **كيفية العمل:**
|
||||||
|
1. عند وصول إشعار FCM من نوع `Order` والتطبيق في الخلفية، يتم استدعاء `backgroundMessageHandler`.
|
||||||
|
2. يتم التحقق من إذن الرسم فوق التطبيقات. إذا مُنح، يتم استدعاء `FlutterOverlayWindow.showOverlay` وتمرير بيانات الطلب (`DriverList`).
|
||||||
|
3. ملف `order_over_lay.dart` هو تطبيق Flutter مصغر يعمل بشكل مستقل فوق التطبيقات الأخرى. يحتوي على منطق قبول/رفض خاص به ويتصل بالسيرفر مباشرة.
|
||||||
|
|
||||||
|
### ب. منطق القبول والرفض (`OrderRequestController`)
|
||||||
|
|
||||||
|
- **المسار:** `lib/controller/home/captin/order_request_controller.dart`.
|
||||||
|
- **العداد التنازلي:** يتم تشغيل `startTimer` لمدة 15-20 ثانية. إذا انتهى الوقت، يتم رفض الطلب تلقائياً.
|
||||||
|
- **القبول:** عند الضغط على "قبول"، يتم استدعاء `updateStausFromSpeed.php` لحجز الطلب ومنع تضارب السائقين (Race Condition).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 🚗 تنفيذ الرحلة (Trip Execution Workflow)
|
||||||
|
|
||||||
|
يدير `MapDriverController` دورة حياة الرحلة بالكامل.
|
||||||
|
|
||||||
|
### أ. الذهاب للراكب (`Going to Passenger`)
|
||||||
|
|
||||||
|
- عند قبول الطلب، تتغير الحالة إلى `Apply`.
|
||||||
|
- يتم عرض موقع الراكب على الخريطة ورسم المسار باستخدام `getRoute` (يعتمد على OSRM أو Google Directions).
|
||||||
|
- يمكن للسائق فتح خرائط جوجل الخارجية عبر `openGoogleMapFromDriverToPassenger`.
|
||||||
|
|
||||||
|
### ب. الوصول (`Arrived`)
|
||||||
|
|
||||||
|
- **الزر:** `I Arrive`.
|
||||||
|
- **المنطق البرمجي:** يتحقق الكود أولاً من المسافة بين السائق والراكب. إذا كانت أقل من **140 متر** (`distance < 140`)، يتم إرسال إشعار `Hi ,I Arrive your site` للراكب وتفعيل عداد الانتظار.
|
||||||
|
|
||||||
|
### ج. بدء وإنهاء الرحلة
|
||||||
|
|
||||||
|
- **البدء (`Begin`):** يطلب النظام تأكيداً "Is the Passenger in your Car?". عند الموافقة، يتم إرسال `status: Begin` للسيرفر ويبدأ عداد الرحلة الفعلي `rideIsBeginPassengerTimer`.
|
||||||
|
- **الإنهاء (`Finish`):**
|
||||||
|
- يتم حساب المسافة المقطوعة والوقت.
|
||||||
|
- يتم استدعاء `finishRideFromDriver1` التي تقوم بإرسال طلبين متزامنين: تحديث حالة الرحلة وتنفيذ عملية الدفع `process_ride_payments.php`.
|
||||||
|
- يتم تحويل السائق لصفحة تقييم الراكب `RatePassenger`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 💰 المحفظة والأرباح (Wallet & Earnings)
|
||||||
|
|
||||||
|
- **المسار:** `lib/controller/home/payment/captain_wallet_controller.dart`.
|
||||||
|
|
||||||
|
### أ. عرض الرصيد والديون
|
||||||
|
|
||||||
|
- يتم جلب البيانات المالية عبر `getCaptainWalletFromRide` (للأرباح من الرحلات) و `getCaptainWalletFromBuyPoints` (للنقاط المشتراة).
|
||||||
|
- **الحظر المالي:** إذا انخفض رصيد النقاط عن حد معين (مثل -300)، يتم منع السائق من استقبال الطلبات ويظهر زر `Charge your Account`.
|
||||||
|
|
||||||
|
### ب. آلية الشحن
|
||||||
|
|
||||||
|
- يتم دعم بوابات دفع متعددة مثل **Syriatel Cash** و **MTN Cash**.
|
||||||
|
- عند الشحن، يتم إنشاء `Invoice` وانتظار الـ Callback أو التحقق الدوري (`polling`) من حالة الدفع.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. ⚙️ الإعدادات والميزات الإضافية
|
||||||
|
|
||||||
|
- **إعدادات السيارة:** تدار عبر `DriverCarController` و `CarsInsertingPage`، حيث يمكن إضافة سيارات جديدة ورفع وثائقها.
|
||||||
|
- **اللغة:** `LocaleController` يدير التبديل بين العربية والإنجليزية ويحفظ التفضيل في `GetStorage`.
|
||||||
|
- **التقييم:** يتم جلب تقييم السائق عبر `getDriverRate` وعرضه في القائمة الجانبية أو الرأسية.
|
||||||
@@ -14,11 +14,10 @@ class SplashScreen extends StatelessWidget {
|
|||||||
final textTheme = Theme.of(context).textTheme;
|
final textTheme = Theme.of(context).textTheme;
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
|
|
||||||
// Define our new green color palette here
|
// A modern, elegant color palette
|
||||||
const Color primaryGreen = Color(0xFF1A535C);
|
const Color primaryDark = Color(0xFF0D1B2A);
|
||||||
const Color secondaryGreen = Color(0xFF4ECDC4);
|
const Color secondaryDark = Color(0xFF1B263B);
|
||||||
const Color logoBackgroundColor = Colors.white;
|
const Color accentColor = Color(0xFF4ECDC4);
|
||||||
const Color logoIconColor = primaryGreen;
|
|
||||||
const Color textColor = Colors.white;
|
const Color textColor = Colors.white;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -30,90 +29,105 @@ class SplashScreen extends StatelessWidget {
|
|||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [
|
colors: [
|
||||||
primaryGreen,
|
primaryDark,
|
||||||
secondaryGreen,
|
secondaryDark,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Centered Logo and Slogan with Animation
|
// Center-aligned animated content
|
||||||
Center(
|
Center(
|
||||||
child: AnimatedBuilder(
|
|
||||||
animation: splashScreenController.animation,
|
|
||||||
builder: (context, child) => FadeTransition(
|
|
||||||
opacity: splashScreenController.animation,
|
|
||||||
child: Transform.translate(
|
|
||||||
offset: Offset(
|
|
||||||
0, 50 * (1 - splashScreenController.animation.value)),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Elegant and Clear Logo
|
// Logo with Scale and Fade animation
|
||||||
Container(
|
ScaleTransition(
|
||||||
padding: const EdgeInsets.all(25),
|
scale: splashScreenController.animation,
|
||||||
decoration: const BoxDecoration(
|
child: FadeTransition(
|
||||||
|
opacity: splashScreenController.animation,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: logoBackgroundColor,
|
color: Colors.white.withOpacity(0.95),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black26,
|
color: accentColor.withOpacity(0.2),
|
||||||
blurRadius: 10,
|
blurRadius: 25,
|
||||||
offset: Offset(0, 4),
|
spreadRadius: 5,
|
||||||
)
|
|
||||||
]),
|
|
||||||
child: Image.asset('assets/images/logo.gif'),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
],
|
||||||
// App Name - now using a variable for color
|
),
|
||||||
Text(
|
child: ClipRRect(
|
||||||
'Intaleq', // Replace with AppInformation.appName if needed
|
borderRadius: BorderRadius.circular(100),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.gif',
|
||||||
|
width: size.width * 0.3, // Responsive size
|
||||||
|
height: size.width * 0.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
// App Name and Slogan with staggered animation
|
||||||
|
_AnimatedText(
|
||||||
|
text: 'Intaleq', // Your App Name
|
||||||
|
animation: splashScreenController.animation,
|
||||||
style: textTheme.headlineMedium?.copyWith(
|
style: textTheme.headlineMedium?.copyWith(
|
||||||
color: textColor,
|
color: textColor,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
letterSpacing: 2,
|
letterSpacing: 3,
|
||||||
),
|
),
|
||||||
|
beginOffset: const Offset(0, 0.5),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// Slogan - now using a variable for color
|
_AnimatedText(
|
||||||
Text(
|
text: 'Your Journey Begins Here'.tr,
|
||||||
'Your Journey Begins Here'.tr,
|
animation: splashScreenController.animation,
|
||||||
style: textTheme.titleMedium?.copyWith(
|
style: textTheme.titleMedium?.copyWith(
|
||||||
color: textColor.withOpacity(0.9),
|
color: textColor.withOpacity(0.8),
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
),
|
),
|
||||||
|
beginOffset: const Offset(0, 0.8),
|
||||||
|
startDelay: 0.2, // Start after the title
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
// Bottom Version Info and Progress Bar
|
||||||
|
|
||||||
// Bottom Loading Bar and Version Info
|
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
bottom: size.height * 0.05, left: 40, right: 40),
|
bottom: size.height * 0.06,
|
||||||
|
left: 40,
|
||||||
|
right: 40,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Obx(() => ClipRRect(
|
Obx(
|
||||||
|
() => ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: splashScreenController.progress.value,
|
value: splashScreenController.progress.value,
|
||||||
backgroundColor: textColor.withOpacity(0.2),
|
backgroundColor: primaryDark.withOpacity(0.5),
|
||||||
valueColor:
|
valueColor:
|
||||||
const AlwaysStoppedAnimation<Color>(textColor),
|
const AlwaysStoppedAnimation<Color>(accentColor),
|
||||||
minHeight: 5,
|
minHeight: 6,
|
||||||
),
|
),
|
||||||
)),
|
),
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
Text(
|
const SizedBox(height: 20),
|
||||||
'Version: ${box.read(BoxName.packagInfo) ?? '1.0.0'}',
|
GetBuilder<SplashScreenController>(
|
||||||
|
builder: (controller) => Text(
|
||||||
|
'Version: ${controller.packageInfo.isNotEmpty ? controller.packageInfo : '...'}',
|
||||||
style: textTheme.bodySmall?.copyWith(
|
style: textTheme.bodySmall?.copyWith(
|
||||||
color: textColor.withOpacity(0.7),
|
color: textColor.withOpacity(0.5),
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -126,3 +140,40 @@ class SplashScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A helper widget for creating staggered text animations.
|
||||||
|
class _AnimatedText extends StatelessWidget {
|
||||||
|
const _AnimatedText({
|
||||||
|
required this.animation,
|
||||||
|
required this.text,
|
||||||
|
required this.style,
|
||||||
|
required this.beginOffset,
|
||||||
|
this.startDelay = 0.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Animation<double> animation;
|
||||||
|
final String text;
|
||||||
|
final TextStyle? style;
|
||||||
|
final Offset beginOffset;
|
||||||
|
final double startDelay;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: Interval(startDelay, 1.0, curve: Curves.easeOut),
|
||||||
|
),
|
||||||
|
child: SlideTransition(
|
||||||
|
position: Tween<Offset>(
|
||||||
|
begin: beginOffset,
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: Interval(startDelay, 1.0, curve: Curves.easeOut),
|
||||||
|
)),
|
||||||
|
child: Text(text, style: style),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,59 +3,66 @@ import 'package:sefer_driver/views/widgets/my_textField.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart'; // Import this for formatting
|
||||||
import 'package:sefer_driver/constant/colors.dart';
|
import 'package:sefer_driver/constant/colors.dart';
|
||||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||||
|
|
||||||
import '../../constant/style.dart';
|
import '../../constant/style.dart';
|
||||||
import '../../controller/rate/rate_conroller.dart';
|
import '../../controller/rate/rate_conroller.dart';
|
||||||
|
|
||||||
// Changed: تم إعادة بناء الصفحة بالكامل لتحسين التصميم وتجربة المستخدم
|
|
||||||
class RatePassenger extends StatelessWidget {
|
class RatePassenger extends StatelessWidget {
|
||||||
final RateController controller = Get.put(RateController());
|
final RateController controller = Get.put(RateController());
|
||||||
|
|
||||||
|
// Format: 1,234.5
|
||||||
|
final NumberFormat currencyFormatter = NumberFormat("#,##0.0", "en_US");
|
||||||
|
|
||||||
RatePassenger({super.key});
|
RatePassenger({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// New: استخدام Scaffold القياسي لهيكل أكثر قوة ومرونة
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.grey[50],
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Rate Passenger'.tr),
|
title: Text('Trip Completed'.tr,
|
||||||
|
style: const TextStyle(color: Colors.black)),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
automaticallyImplyLeading: false, // New: إزالة سهم الرجوع
|
automaticallyImplyLeading: false,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
elevation: 1,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
// New: استخدام GetBuilder على مستوى الجسم لضمان تحديث الواجهة
|
|
||||||
body: GetBuilder<RateController>(
|
body: GetBuilder<RateController>(
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
// New: استخدام SingleChildScrollView لتجنب مشاكل الـ overflow عند ظهور لوحة المفاتيح
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// New: استدعاء ودجت منفصلة لكل قسم لزيادة التنظيم
|
// 1. The HERO Section: Big Price Display
|
||||||
_buildPriceSummaryCard(context, controller),
|
_buildHeroPriceDisplay(context),
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// New: قسم المحفظة يظهر فقط إذا لم يتم التحقق منه
|
|
||||||
if (controller.walletChecked != 'true')
|
|
||||||
_buildWalletSection(context, controller),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
_buildRatingSection(context, controller),
|
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
MyElevatedButton(
|
// 2. Wallet Section (Conditional)
|
||||||
title: 'Submit rating'.tr,
|
if (controller.walletChecked != 'true')
|
||||||
|
_buildWalletSection(context, controller),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 3. Rating Section
|
||||||
|
_buildRatingSection(context, controller),
|
||||||
|
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// 4. Submit Button
|
||||||
|
SizedBox(
|
||||||
|
height: 55,
|
||||||
|
child: MyElevatedButton(
|
||||||
|
title: 'Finish & Submit'.tr,
|
||||||
onPressed: () => controller.addRateToPassenger(),
|
onPressed: () => controller.addRateToPassenger(),
|
||||||
// New: جعل الزر يأخذ العرض الكامل لمزيد من الوضوح
|
|
||||||
// isFullWidth: true,
|
// isFullWidth: true,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -65,85 +72,96 @@ class RatePassenger extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// New: ودجت منفصلة لعرض بطاقة ملخص السعر
|
// --- WIDGETS ---
|
||||||
Widget _buildPriceSummaryCard(
|
|
||||||
BuildContext context, RateController controller) {
|
|
||||||
final MapDriverController mapController = Get.find<MapDriverController>();
|
|
||||||
final double originalPrice =
|
|
||||||
double.tryParse(controller.price.toString()) ?? 0.0;
|
|
||||||
final double priceAfterDiscount = originalPrice - (originalPrice * 0.12);
|
|
||||||
|
|
||||||
return Card(
|
Widget _buildHeroPriceDisplay(BuildContext context) {
|
||||||
elevation: 4,
|
final MapDriverController mapController = Get.find<MapDriverController>();
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
child: Padding(
|
// Parse the string to double to format it correctly
|
||||||
padding: const EdgeInsets.all(16.0),
|
double amount =
|
||||||
|
double.tryParse(mapController.paymentAmount.toString()) ?? 0.0;
|
||||||
|
String formattedAmount = currencyFormatter.format(amount);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.primaryColor, // Use your brand color or a dark color
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: AppColor.primaryColor.withOpacity(0.3),
|
||||||
|
blurRadius: 15,
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${'Trip Summary with'.tr} ${mapController.passengerName}',
|
'Collect Cash'.tr.toUpperCase(),
|
||||||
style: AppStyle.title
|
style: const TextStyle(
|
||||||
.copyWith(fontSize: 18, fontWeight: FontWeight.bold),
|
color: Colors.white70,
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const Divider(height: 24, thickness: 1),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text('Original Fare'.tr, style: AppStyle.title),
|
|
||||||
Text(
|
|
||||||
priceAfterDiscount.toStringAsFixed(2),
|
|
||||||
style: AppStyle.number.copyWith(
|
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: AppColor.redColor,
|
fontWeight: FontWeight.w600,
|
||||||
decoration: TextDecoration.lineThrough,
|
letterSpacing: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Currency Symbol (Small)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
'SYP'.tr, // Replace with your local currency symbol if needed
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.8),
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
// The Price (Huge)
|
||||||
|
Text(
|
||||||
|
formattedAmount,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 56, // Very Large Font
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
height: 1.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 10),
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text('Your Earnings'.tr,
|
|
||||||
style:
|
|
||||||
AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
|
||||||
Container(
|
Container(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.greenColor.withOpacity(0.1),
|
color: Colors.white.withOpacity(0.2),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(20),
|
||||||
border: Border.all(color: AppColor.greenColor),
|
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
mapController.paymentAmount,
|
'Passenger: ${mapController.passengerName}',
|
||||||
style: AppStyle.number
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||||
.copyWith(color: AppColor.greenColor, fontSize: 20),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
'Exclusive offers and discounts always with the Sefer app'.tr,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: AppStyle.title
|
|
||||||
.copyWith(color: AppColor.redColor, fontSize: 13),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// New: ودجت منفصلة لقسم الدفع عبر المحفظة
|
|
||||||
Widget _buildWalletSection(BuildContext context, RateController controller) {
|
Widget _buildWalletSection(BuildContext context, RateController controller) {
|
||||||
return Card(
|
return Container(
|
||||||
elevation: 4,
|
decoration: BoxDecoration(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: controller.ispassengerWantWalletFromDriver
|
child: controller.ispassengerWantWalletFromDriver
|
||||||
@@ -154,37 +172,61 @@ class RatePassenger extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// New: واجهة سؤال استخدام المحفظة
|
|
||||||
Widget _buildWalletQuery(RateController controller) {
|
Widget _buildWalletQuery(RateController controller) {
|
||||||
return Column(
|
return Column(
|
||||||
key: const ValueKey('walletQuery'),
|
key: const ValueKey('walletQuery'),
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
"Would the passenger like to settle the remaining fare using their wallet?"
|
children: [
|
||||||
.tr,
|
Container(
|
||||||
style: AppStyle.title,
|
padding: const EdgeInsets.all(10),
|
||||||
textAlign: TextAlign.center,
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
child: const Icon(Icons.account_balance_wallet,
|
||||||
|
color: Colors.orange),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
"Pay remaining to Wallet?".tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: MyElevatedButton(
|
child: OutlinedButton(
|
||||||
title: 'No'.tr,
|
onPressed: () {}, // Optional logic
|
||||||
onPressed: () {
|
style: OutlinedButton.styleFrom(
|
||||||
// يمكنك هنا تحديد ما يحدث عند الضغط على "لا"
|
foregroundColor: Colors.grey,
|
||||||
// حاليًا، ستبقى الواجهة كما هي أو يمكنك إخفاؤها
|
side: BorderSide(color: Colors.grey.shade300),
|
||||||
},
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
kolor: AppColor.redColor,
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
child: Text('No'.tr),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: MyElevatedButton(
|
child: ElevatedButton(
|
||||||
title: 'Yes, Pay'.tr,
|
onPressed: () => controller.passengerWantPay(),
|
||||||
onPressed: () {
|
style: ElevatedButton.styleFrom(
|
||||||
controller.passengerWantPay();
|
backgroundColor: AppColor.primaryColor,
|
||||||
},
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
child: Text('Yes, Pay'.tr,
|
||||||
|
style: const TextStyle(color: Colors.white)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -193,137 +235,94 @@ class RatePassenger extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// New: واجهة إدخال المبلغ المدفوع
|
|
||||||
Widget _buildAmountInput(RateController controller) {
|
Widget _buildAmountInput(RateController controller) {
|
||||||
return Column(
|
return Column(
|
||||||
key: const ValueKey('amountInput'),
|
key: const ValueKey('amountInput'),
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"How much Passenger pay?".tr,
|
"Enter Amount Paid".tr,
|
||||||
style: AppStyle.title,
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Form(
|
Form(
|
||||||
key: controller.formKey,
|
key: controller.formKey,
|
||||||
child: MyTextForm(
|
child: MyTextForm(
|
||||||
controller: controller.passengerPayAmount,
|
controller: controller.passengerPayAmount,
|
||||||
label: "Passenger paid amount".tr,
|
label: "Amount".tr,
|
||||||
hint: "0.00",
|
hint: "0.00",
|
||||||
type: const TextInputType.numberWithOptions(decimal: true),
|
type: const TextInputType.numberWithOptions(decimal: true),
|
||||||
|
// Suggestion: Add a suffix icon for currency if available in your widget
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
MyElevatedButton(
|
SizedBox(
|
||||||
title: "Add to Passenger Wallet".tr,
|
width: double.infinity,
|
||||||
// isFullWidth: true,
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () => controller.addPassengerWallet(),
|
||||||
controller.addPassengerWallet();
|
style: ElevatedButton.styleFrom(
|
||||||
},
|
backgroundColor: Colors.green,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
child: Text("Confirm Payment".tr,
|
||||||
|
style: const TextStyle(color: Colors.white)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// New: ودجت منفصلة لقسم التقييم وكتابة الملاحظات
|
|
||||||
Widget _buildRatingSection(BuildContext context, RateController controller) {
|
Widget _buildRatingSection(BuildContext context, RateController controller) {
|
||||||
return Card(
|
return Column(
|
||||||
elevation: 4,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
Text('How was the passenger?'.tr,
|
Text(
|
||||||
style: AppStyle.title
|
'Rate Passenger'.tr,
|
||||||
.copyWith(fontSize: 18, fontWeight: FontWeight.bold)),
|
style: TextStyle(
|
||||||
const SizedBox(height: 20),
|
color: Colors.grey[600],
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
RatingBar.builder(
|
RatingBar.builder(
|
||||||
initialRating: 0,
|
initialRating: 0,
|
||||||
|
minRating: 1,
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
allowHalfRating: false,
|
||||||
itemCount: 5,
|
itemCount: 5,
|
||||||
itemSize: 40,
|
itemSize: 45, // Large stars
|
||||||
itemPadding: const EdgeInsets.symmetric(horizontal: 4),
|
itemPadding: const EdgeInsets.symmetric(horizontal: 2.0),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, _) => const Icon(
|
||||||
switch (index) {
|
Icons.star_rounded,
|
||||||
case 0:
|
color: Colors.amber,
|
||||||
return const Icon(Icons.sentiment_very_dissatisfied,
|
),
|
||||||
color: Colors.red);
|
|
||||||
case 1:
|
|
||||||
return const Icon(Icons.sentiment_dissatisfied,
|
|
||||||
color: Colors.redAccent);
|
|
||||||
case 2:
|
|
||||||
return const Icon(Icons.sentiment_neutral,
|
|
||||||
color: Colors.amber);
|
|
||||||
case 3:
|
|
||||||
return const Icon(Icons.sentiment_satisfied,
|
|
||||||
color: Colors.lightGreen);
|
|
||||||
case 4:
|
|
||||||
return const Icon(Icons.sentiment_very_satisfied,
|
|
||||||
color: Colors.green);
|
|
||||||
default:
|
|
||||||
return const Icon(Icons.sentiment_neutral,
|
|
||||||
color: Colors.amber);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRatingUpdate: (rating) {
|
onRatingUpdate: (rating) {
|
||||||
controller.selectRateItem(rating);
|
controller.selectRateItem(rating);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 20),
|
||||||
TextFormField(
|
// Simplified comment box
|
||||||
maxLines: 4,
|
TextField(
|
||||||
minLines: 2,
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
controller: controller.comment,
|
controller: controller.comment,
|
||||||
|
maxLines: 2,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Add a comment (optional)'.tr,
|
hintText: 'Any comments about the passenger?'.tr,
|
||||||
hintText: 'Type something...'.tr,
|
filled: true,
|
||||||
prefixIcon: const Icon(Icons.rate_review_outlined),
|
fillColor: Colors.white,
|
||||||
border: const OutlineInputBorder(
|
contentPadding:
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.grey.shade200),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New: إضافة isFullWidth إلى MyElevatedButton لتسهيل التحكم في العرض
|
|
||||||
// تأكد من تحديث ملف elevated_btn.dart بهذا التغيير
|
|
||||||
/*
|
|
||||||
class MyElevatedButton extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
final VoidCallback onPressed;
|
|
||||||
final Color? kolor;
|
|
||||||
final bool isFullWidth; // New property
|
|
||||||
|
|
||||||
const MyElevatedButton({
|
|
||||||
Key? key,
|
|
||||||
required this.title,
|
|
||||||
required this.onPressed,
|
|
||||||
this.kolor,
|
|
||||||
this.isFullWidth = false, // Default value
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
width: isFullWidth ? double.infinity : null, // Apply width
|
|
||||||
height: 50, // Standard height
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: onPressed,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: kolor ?? AppColor.primaryColor,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
textStyle: AppStyle.title.copyWith(color: Colors.white),
|
|
||||||
),
|
|
||||||
child: Text(title, style: const TextStyle(color: Colors.white)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class ContactUsPage extends StatelessWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Get.put(TextToSpeechController()).speakText(
|
Get.put(TextToSpeechController()).speakText(
|
||||||
'Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 15%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.'
|
'Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 8%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.'
|
||||||
.tr);
|
.tr);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.headphones),
|
icon: const Icon(Icons.headphones),
|
||||||
@@ -43,7 +43,7 @@ class ContactUsPage extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 15%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.'
|
'Intaleq is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 8%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.'
|
||||||
.tr,
|
.tr,
|
||||||
style: AppStyle.title,
|
style: AppStyle.title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@@ -60,7 +60,7 @@ class ContactUsPage extends StatelessWidget {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"You can contact us during working hours from 12:00 - 19:00."
|
"You can contact us during working hours from 10:00 - 17:00."
|
||||||
.tr,
|
.tr,
|
||||||
style: AppStyle.title,
|
style: AppStyle.title,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:sefer_driver/views/auth/captin/contact_us_page.dart';
|
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -18,20 +16,45 @@ import '../../../constant/style.dart';
|
|||||||
import '../../../controller/auth/apple_sigin.dart';
|
import '../../../controller/auth/apple_sigin.dart';
|
||||||
import '../../../controller/auth/captin/login_captin_controller.dart';
|
import '../../../controller/auth/captin/login_captin_controller.dart';
|
||||||
import '../../../controller/auth/google_sign.dart';
|
import '../../../controller/auth/google_sign.dart';
|
||||||
import '../../../controller/functions/encrypt_decrypt.dart';
|
|
||||||
import '../../../controller/functions/overlay_permisssion.dart';
|
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../../print.dart';
|
import '../../../print.dart';
|
||||||
import '../../widgets/elevated_btn.dart';
|
import '../../widgets/elevated_btn.dart';
|
||||||
import '../../widgets/mycircular.dart';
|
import '../../widgets/mycircular.dart';
|
||||||
import '../country_widget.dart';
|
import 'contact_us_page.dart';
|
||||||
import 'otp_page.dart';
|
import 'otp_page.dart'; // تأكد من وجود هذا الملف لديك
|
||||||
|
|
||||||
class LoginCaptin extends StatelessWidget {
|
class LoginCaptin extends StatefulWidget {
|
||||||
|
const LoginCaptin({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LoginCaptin> createState() => _LoginCaptinState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginCaptinState extends State<LoginCaptin> with WidgetsBindingObserver {
|
||||||
final AuthController authController = Get.put(AuthController());
|
final AuthController authController = Get.put(AuthController());
|
||||||
final LoginDriverController controller = Get.put(LoginDriverController());
|
final LoginDriverController controller = Get.put(LoginDriverController());
|
||||||
|
|
||||||
LoginCaptin({super.key});
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
|
// فحص الإذن عند فتح الصفحة مباشرة
|
||||||
|
controller.getLocationPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
// التحقق عند العودة من الإعدادات
|
||||||
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
controller.getLocationPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -49,289 +72,25 @@ class LoginCaptin extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines which UI to show based on the driver's progress (agreement, permissions, login).
|
|
||||||
Widget _buildBodyContent(
|
Widget _buildBodyContent(
|
||||||
BuildContext context, LoginDriverController controller) {
|
BuildContext context, LoginDriverController controller) {
|
||||||
|
// 1. صفحة الموافقة على الشروط
|
||||||
if (box.read(BoxName.agreeTerms) != 'agreed') {
|
if (box.read(BoxName.agreeTerms) != 'agreed') {
|
||||||
return _buildAgreementPage(context, controller);
|
return _buildAgreementPage(context, controller);
|
||||||
}
|
}
|
||||||
// if (box.read(BoxName.countryCode) == null) {
|
|
||||||
// return CountryPicker(); // Assumed to be a full-screen widget
|
// 2. صفحة إذن الموقع
|
||||||
// }
|
|
||||||
if (box.read(BoxName.locationPermission) != 'true') {
|
if (box.read(BoxName.locationPermission) != 'true') {
|
||||||
return _buildLocationPermissionPage(context, controller);
|
return _buildLocationPermissionPage(context, controller);
|
||||||
}
|
}
|
||||||
// Once all permissions are granted, show the main login UI
|
|
||||||
|
// 3. صفحة تسجيل الدخول (رقم الهاتف)
|
||||||
return PhoneNumberScreen();
|
return PhoneNumberScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Redesigned UI for the main login screen.
|
// --- صفحة الشروط والأحكام ---
|
||||||
Widget _buildLoginUI(BuildContext context, LoginDriverController controller) {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Image.asset('assets/images/logo.gif', height: 120, width: 120),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Text(
|
|
||||||
'Driver Portal'.tr,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: AppStyle.headTitle2.copyWith(fontSize: 28),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Sign in to start your journey'.tr,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: AppStyle.subtitle,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 40),
|
|
||||||
|
|
||||||
// Conditional UI based on the controller state
|
|
||||||
if (controller.isGoogleDashOpen)
|
|
||||||
_buildManualLoginForm(context, controller, isRegistration: true)
|
|
||||||
else if (Platform.isIOS && controller.isTest == 0)
|
|
||||||
_buildManualLoginForm(context, controller, isRegistration: false)
|
|
||||||
else
|
|
||||||
_buildSocialLoginOptions(context, controller),
|
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
Center(
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => Get.to(() => ContactUsPage()),
|
|
||||||
child: Text(
|
|
||||||
'Need help? Contact Us'.tr,
|
|
||||||
style: AppStyle.subtitle.copyWith(
|
|
||||||
color: AppColor.blueColor,
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds the social login buttons (Google, Apple, and manual option).
|
|
||||||
Widget _buildSocialLoginOptions(
|
|
||||||
BuildContext context, LoginDriverController controller) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Sign in with a provider for easy access'.tr,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: AppStyle.title,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildSocialButton(
|
|
||||||
text: 'Sign In with Google'.tr,
|
|
||||||
icon: FontAwesome.google,
|
|
||||||
backgroundColor: AppColor.redColor,
|
|
||||||
onPressed: () async {
|
|
||||||
GoogleSignInHelper().signInFromLogin();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (Platform.isIOS) ...[
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildSocialButton(
|
|
||||||
text: 'Sign in with Apple'.tr,
|
|
||||||
icon: Icons.apple,
|
|
||||||
backgroundColor: Colors.black,
|
|
||||||
onPressed: () async {
|
|
||||||
User? user = await authController.signInWithApple();
|
|
||||||
if (user != null) {
|
|
||||||
box.write(BoxName.emailDriver, user.email.toString());
|
|
||||||
box.write(BoxName.driverID, user.uid);
|
|
||||||
controller.loginWithGoogleCredential(
|
|
||||||
user.uid,
|
|
||||||
user.email.toString(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Expanded(child: Divider()),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
||||||
child: Text('Or'.tr, style: AppStyle.subtitle),
|
|
||||||
),
|
|
||||||
const Expanded(child: Divider()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
MyElevatedButton(
|
|
||||||
title: 'Create Account with Email'.tr,
|
|
||||||
onPressed: () => controller.changeGoogleDashOpen(),
|
|
||||||
kolor: AppColor.blueColor, // Using 'kolor' as in your widget
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds the form for manual email/password login or registration.
|
|
||||||
Widget _buildManualLoginForm(
|
|
||||||
BuildContext context, LoginDriverController controller,
|
|
||||||
{required bool isRegistration}) {
|
|
||||||
return Card(
|
|
||||||
elevation: 8,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(20.0),
|
|
||||||
child: Form(
|
|
||||||
key: controller.formKey,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
isRegistration ? 'Create Driver Account'.tr : 'Driver Login'.tr,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: AppStyle.headTitle2,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_buildTextFormField(
|
|
||||||
controller: controller.emailController,
|
|
||||||
labelText: 'Email'.tr,
|
|
||||||
hintText: 'Enter your email'.tr,
|
|
||||||
prefixIcon: Icons.email_outlined,
|
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || !GetUtils.isEmail(value)) {
|
|
||||||
return 'Please enter a valid email'.tr;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
GetBuilder<LoginDriverController>(
|
|
||||||
id: 'passwordVisibility', // ID to only rebuild this widget
|
|
||||||
builder: (_) => _buildTextFormField(
|
|
||||||
controller: controller.passwordController,
|
|
||||||
labelText: 'Password'.tr,
|
|
||||||
hintText: 'Enter your password'.tr,
|
|
||||||
prefixIcon: Icons.lock_outline,
|
|
||||||
obscureText: controller.isPasswordHidden,
|
|
||||||
suffixIcon: IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
controller.isPasswordHidden
|
|
||||||
? Icons.visibility_off
|
|
||||||
: Icons.visibility,
|
|
||||||
color: AppColor.primaryColor,
|
|
||||||
),
|
|
||||||
onPressed: () => controller.togglePasswordVisibility(),
|
|
||||||
),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.length < 6) {
|
|
||||||
return 'Password must be at least 6 characters'.tr;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 30),
|
|
||||||
controller.isloading
|
|
||||||
? const Center(child: MyCircularProgressIndicator())
|
|
||||||
: MyElevatedButton(
|
|
||||||
title:
|
|
||||||
(isRegistration ? 'Create Account'.tr : 'Login'.tr),
|
|
||||||
onPressed: () {
|
|
||||||
if (controller.formKey.currentState!.validate()) {
|
|
||||||
if (isRegistration) {
|
|
||||||
String email = controller.emailController.text;
|
|
||||||
String uniqueId =
|
|
||||||
controller.generateUniqueIdFromEmail(email);
|
|
||||||
box.write(BoxName.driverID, uniqueId);
|
|
||||||
box.write(BoxName.emailDriver, email);
|
|
||||||
controller.loginUsingCredentialsWithoutGoogle(
|
|
||||||
controller.passwordController.text,
|
|
||||||
email,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// This is the flow for iOS manual login
|
|
||||||
controller.loginWithGoogleCredential(
|
|
||||||
controller.passwordController.text,
|
|
||||||
controller.emailController.text,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (isRegistration) // Show back button only on the registration form
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => controller.changeGoogleDashOpen(),
|
|
||||||
child: Text(
|
|
||||||
'Back to other sign-in options'.tr,
|
|
||||||
style: TextStyle(color: AppColor.primaryColor),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A helper method to create styled TextFormFields.
|
|
||||||
TextFormField _buildTextFormField({
|
|
||||||
required TextEditingController controller,
|
|
||||||
required String labelText,
|
|
||||||
required String hintText,
|
|
||||||
required IconData prefixIcon,
|
|
||||||
required String? Function(String?) validator,
|
|
||||||
bool obscureText = false,
|
|
||||||
Widget? suffixIcon,
|
|
||||||
TextInputType keyboardType = TextInputType.text,
|
|
||||||
}) {
|
|
||||||
return TextFormField(
|
|
||||||
controller: controller,
|
|
||||||
validator: validator,
|
|
||||||
obscureText: obscureText,
|
|
||||||
keyboardType: keyboardType,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: labelText,
|
|
||||||
hintText: hintText,
|
|
||||||
prefixIcon: Icon(prefixIcon, color: AppColor.primaryColor),
|
|
||||||
suffixIcon: suffixIcon,
|
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.0)),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
|
||||||
borderSide:
|
|
||||||
const BorderSide(color: AppColor.primaryColor, width: 2.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A helper for creating consistent social login buttons.
|
|
||||||
Widget _buildSocialButton({
|
|
||||||
required String text,
|
|
||||||
required IconData icon,
|
|
||||||
required Color backgroundColor,
|
|
||||||
required VoidCallback onPressed,
|
|
||||||
}) {
|
|
||||||
return ElevatedButton.icon(
|
|
||||||
icon: Icon(icon, color: Colors.white),
|
|
||||||
label:
|
|
||||||
Text(text, style: const TextStyle(color: Colors.white, fontSize: 16)),
|
|
||||||
onPressed: onPressed,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Redesigned UI for the Terms and Conditions agreement page.
|
|
||||||
Widget _buildAgreementPage(
|
Widget _buildAgreementPage(
|
||||||
BuildContext context, LoginDriverController controller) {
|
BuildContext context, LoginDriverController controller) {
|
||||||
// This UI can be identical to the one in LoginPage for consistency.
|
|
||||||
// I am reusing the improved design from the previous request.
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -404,7 +163,7 @@ class LoginCaptin extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Redesigned UI for the Location Permission request page.
|
// --- صفحة إذن الموقع ---
|
||||||
Widget _buildLocationPermissionPage(
|
Widget _buildLocationPermissionPage(
|
||||||
BuildContext context, LoginDriverController controller) {
|
BuildContext context, LoginDriverController controller) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@@ -439,14 +198,33 @@ class LoginCaptin extends StatelessWidget {
|
|||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
MyElevatedButton(
|
MyElevatedButton(
|
||||||
title: "Allow Location Access".tr,
|
title: "Allow Location Access".tr,
|
||||||
|
kolor: AppColor.greenColor,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await getLocationPermission(); // Assumes this function handles the request logic
|
|
||||||
if (await Permission.location.isGranted) {
|
|
||||||
box.write(BoxName.locationPermission, 'true');
|
box.write(BoxName.locationPermission, 'true');
|
||||||
controller.update(); // Re-check conditions
|
controller.update();
|
||||||
|
// 1. طلب إذن الموقع العادي (أثناء الاستخدام) أولاً
|
||||||
|
var status = await Permission.location.request();
|
||||||
|
|
||||||
|
if (status.isGranted) {
|
||||||
|
// 2. إذا كنت تحتاج "طوال الوقت" (Background)، اطلبه الآن بشكل منفصل
|
||||||
|
// ملاحظة: في أندرويد 11+ سينقلك هذا لصفحة إعدادات خاصة
|
||||||
|
var backgroundStatus =
|
||||||
|
await Permission.locationAlways.request();
|
||||||
|
|
||||||
|
if (backgroundStatus.isGranted) {
|
||||||
|
box.write(BoxName.locationPermission, 'true');
|
||||||
|
controller.update();
|
||||||
|
} else {
|
||||||
|
// المستخدم وافق على "أثناء الاستخدام" فقط، يمكنك المشي في الحال
|
||||||
|
// أو إجباره، حسب منطق تطبيقك (تطبيق سائق يفضل Always)
|
||||||
|
box.write(BoxName.locationPermission, 'true');
|
||||||
|
controller.update();
|
||||||
|
}
|
||||||
|
} else if (status.isPermanentlyDenied) {
|
||||||
|
// إذا كانت الحالة مرفوضة نهائياً، يجب فتح الإعدادات
|
||||||
|
openAppSettings();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
kolor: AppColor.greenColor,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -458,4 +236,258 @@ class LoginCaptin extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- واجهة تسجيل الدخول اليدوي/الاجتماعي (للاستخدام المستقبلي إذا لزم الأمر) ---
|
||||||
|
Widget _buildLoginUI(BuildContext context, LoginDriverController controller) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 32.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Image.asset('assets/images/logo.gif', height: 120, width: 120),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
'Driver Portal'.tr,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: AppStyle.headTitle2.copyWith(fontSize: 28),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Sign in to start your journey'.tr,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: AppStyle.subtitle,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
if (controller.isGoogleDashOpen)
|
||||||
|
_buildManualLoginForm(context, controller, isRegistration: true)
|
||||||
|
else if (Platform.isIOS && controller.isTest == 0)
|
||||||
|
_buildManualLoginForm(context, controller, isRegistration: false)
|
||||||
|
else
|
||||||
|
_buildSocialLoginOptions(context, controller),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Center(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => Get.to(() => ContactUsPage()),
|
||||||
|
child: Text(
|
||||||
|
'Need help? Contact Us'.tr,
|
||||||
|
style: AppStyle.subtitle.copyWith(
|
||||||
|
color: AppColor.blueColor,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSocialLoginOptions(
|
||||||
|
BuildContext context, LoginDriverController controller) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Sign in with a provider for easy access'.tr,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: AppStyle.title,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildSocialButton(
|
||||||
|
text: 'Sign In with Google'.tr,
|
||||||
|
icon: FontAwesome.google,
|
||||||
|
backgroundColor: AppColor.redColor,
|
||||||
|
onPressed: () async {
|
||||||
|
GoogleSignInHelper().signInFromLogin();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (Platform.isIOS) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildSocialButton(
|
||||||
|
text: 'Sign in with Apple'.tr,
|
||||||
|
icon: Icons.apple,
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
onPressed: () async {
|
||||||
|
User? user = await authController.signInWithApple();
|
||||||
|
if (user != null) {
|
||||||
|
box.write(BoxName.emailDriver, user.email.toString());
|
||||||
|
box.write(BoxName.driverID, user.uid);
|
||||||
|
controller.loginWithGoogleCredential(
|
||||||
|
user.uid,
|
||||||
|
user.email.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(child: Divider()),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: Text('Or'.tr, style: AppStyle.subtitle),
|
||||||
|
),
|
||||||
|
const Expanded(child: Divider()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
MyElevatedButton(
|
||||||
|
title: 'Create Account with Email'.tr,
|
||||||
|
onPressed: () => controller.changeGoogleDashOpen(),
|
||||||
|
kolor: AppColor.blueColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildManualLoginForm(
|
||||||
|
BuildContext context, LoginDriverController controller,
|
||||||
|
{required bool isRegistration}) {
|
||||||
|
return Card(
|
||||||
|
elevation: 8,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Form(
|
||||||
|
key: controller.formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
isRegistration ? 'Create Driver Account'.tr : 'Driver Login'.tr,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: AppStyle.headTitle2,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: controller.emailController,
|
||||||
|
labelText: 'Email'.tr,
|
||||||
|
hintText: 'Enter your email'.tr,
|
||||||
|
prefixIcon: Icons.email_outlined,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || !GetUtils.isEmail(value)) {
|
||||||
|
return 'Please enter a valid email'.tr;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
GetBuilder<LoginDriverController>(
|
||||||
|
id: 'passwordVisibility',
|
||||||
|
builder: (_) => _buildTextFormField(
|
||||||
|
controller: controller.passwordController,
|
||||||
|
labelText: 'Password'.tr,
|
||||||
|
hintText: 'Enter your password'.tr,
|
||||||
|
prefixIcon: Icons.lock_outline,
|
||||||
|
obscureText: controller.isPasswordHidden,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
controller.isPasswordHidden
|
||||||
|
? Icons.visibility_off
|
||||||
|
: Icons.visibility,
|
||||||
|
color: AppColor.primaryColor,
|
||||||
|
),
|
||||||
|
onPressed: () => controller.togglePasswordVisibility(),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.length < 6) {
|
||||||
|
return 'Password must be at least 6 characters'.tr;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
controller.isloading
|
||||||
|
? const Center(child: MyCircularProgressIndicator())
|
||||||
|
: MyElevatedButton(
|
||||||
|
title:
|
||||||
|
(isRegistration ? 'Create Account'.tr : 'Login'.tr),
|
||||||
|
onPressed: () {
|
||||||
|
if (controller.formKey.currentState!.validate()) {
|
||||||
|
if (isRegistration) {
|
||||||
|
String email = controller.emailController.text;
|
||||||
|
String uniqueId =
|
||||||
|
controller.generateUniqueIdFromEmail(email);
|
||||||
|
box.write(BoxName.driverID, uniqueId);
|
||||||
|
box.write(BoxName.emailDriver, email);
|
||||||
|
controller.loginUsingCredentialsWithoutGoogle(
|
||||||
|
controller.passwordController.text,
|
||||||
|
email,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
controller.loginWithGoogleCredential(
|
||||||
|
controller.passwordController.text,
|
||||||
|
controller.emailController.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (isRegistration)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => controller.changeGoogleDashOpen(),
|
||||||
|
child: Text(
|
||||||
|
'Back to other sign-in options'.tr,
|
||||||
|
style: TextStyle(color: AppColor.primaryColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFormField _buildTextFormField({
|
||||||
|
required TextEditingController controller,
|
||||||
|
required String labelText,
|
||||||
|
required String hintText,
|
||||||
|
required IconData prefixIcon,
|
||||||
|
required String? Function(String?) validator,
|
||||||
|
bool obscureText = false,
|
||||||
|
Widget? suffixIcon,
|
||||||
|
TextInputType keyboardType = TextInputType.text,
|
||||||
|
}) {
|
||||||
|
return TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
validator: validator,
|
||||||
|
obscureText: obscureText,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: labelText,
|
||||||
|
hintText: hintText,
|
||||||
|
prefixIcon: Icon(prefixIcon, color: AppColor.primaryColor),
|
||||||
|
suffixIcon: suffixIcon,
|
||||||
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12.0)),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
borderSide:
|
||||||
|
const BorderSide(color: AppColor.primaryColor, width: 2.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSocialButton({
|
||||||
|
required String text,
|
||||||
|
required IconData icon,
|
||||||
|
required Color backgroundColor,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
}) {
|
||||||
|
return ElevatedButton.icon(
|
||||||
|
icon: Icon(icon, color: Colors.white),
|
||||||
|
label:
|
||||||
|
Text(text, style: const TextStyle(color: Colors.white, fontSize: 16)),
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
|
import 'dart:ui';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
|
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
|
||||||
|
|
||||||
|
// --- Placeholder Imports ---
|
||||||
|
// Assuming these files exist in your project structure.
|
||||||
import '../../../constant/box_name.dart';
|
import '../../../constant/box_name.dart';
|
||||||
|
import '../../../constant/colors.dart';
|
||||||
import '../../../controller/auth/captin/phone_helper_controller.dart';
|
import '../../../controller/auth/captin/phone_helper_controller.dart';
|
||||||
import '../../../controller/local/phone_intel/intl_phone_field.dart';
|
import '../../../controller/local/phone_intel/intl_phone_field.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../../print.dart';
|
import '../../../print.dart';
|
||||||
|
// Assuming you have an AppColor class defined in your project.
|
||||||
|
// import 'path/to/your/app_color.dart';
|
||||||
|
|
||||||
|
/// A visually revamped authentication screen with a light, glassy effect,
|
||||||
|
/// themed for the driver application using a green primary color.
|
||||||
class AuthScreen extends StatelessWidget {
|
class AuthScreen extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
final Widget form;
|
final Widget form;
|
||||||
// Using a more neutral, tech-themed animated logo
|
|
||||||
final String logoUrl = 'assets/images/logo.gif';
|
|
||||||
|
|
||||||
const AuthScreen({
|
const AuthScreen({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -22,178 +29,260 @@ class AuthScreen extends StatelessWidget {
|
|||||||
required this.form,
|
required this.form,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Shows a dialog for testers to log in using email and password.
|
||||||
|
void _showTesterLoginDialog(
|
||||||
|
BuildContext context, LoginDriverController controller) {
|
||||||
|
final testerEmailController = TextEditingController();
|
||||||
|
final testerPasswordController = TextEditingController();
|
||||||
|
final testerFormKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: true,
|
||||||
|
builder: (BuildContext dialogContext) {
|
||||||
|
return BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||||
|
child: AlertDialog(
|
||||||
|
backgroundColor: Colors.white.withOpacity(0.85),
|
||||||
|
shape:
|
||||||
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||||
|
title: const Text(
|
||||||
|
'App Tester Login',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style:
|
||||||
|
TextStyle(fontWeight: FontWeight.bold, color: Colors.black87),
|
||||||
|
),
|
||||||
|
content: Form(
|
||||||
|
key: testerFormKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: testerEmailController,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
style: const TextStyle(color: Colors.black),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Email',
|
||||||
|
prefixIcon: const Icon(Icons.email_outlined),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: AppColor.greenColor, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) => value == null || !value.contains('@')
|
||||||
|
? 'Enter a valid email'.tr
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: testerPasswordController,
|
||||||
|
obscureText: true,
|
||||||
|
style: const TextStyle(color: Colors.black),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Password',
|
||||||
|
prefixIcon: const Icon(Icons.lock_outline),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(
|
||||||
|
color: AppColor.greenColor, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: (value) => value == null || value.isEmpty
|
||||||
|
? 'Enter a password'
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColor.greenColor,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child:
|
||||||
|
const Text('Login', style: TextStyle(color: Colors.white)),
|
||||||
|
onPressed: () {
|
||||||
|
if (testerFormKey.currentState!.validate()) {
|
||||||
|
controller.logintest(
|
||||||
|
testerPasswordController.text.trim(),
|
||||||
|
testerEmailController.text.trim(),
|
||||||
|
);
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final controller = Get.put(LoginDriverController());
|
// Controller for the driver's login logic
|
||||||
|
final loginController = Get.put(LoginDriverController());
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
// UPDATED: Changed gradient colors to a green theme
|
// NEW: Light and airy gradient with green accents
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [Colors.teal.shade700, Colors.green.shade600],
|
colors: [
|
||||||
begin: Alignment.topLeft,
|
Colors.white,
|
||||||
end: Alignment.bottomRight,
|
Colors.green.shade50,
|
||||||
|
],
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// Subtle background shapes with the new primary color
|
||||||
|
Positioned(
|
||||||
|
top: -80,
|
||||||
|
left: -100,
|
||||||
|
child: Container(
|
||||||
|
width: 250,
|
||||||
|
height: 250,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: AppColor.greenColor.withOpacity(0.1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: -120,
|
||||||
|
right: -150,
|
||||||
|
child: Container(
|
||||||
|
width: 350,
|
||||||
|
height: 350,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: AppColor.greenColor.withOpacity(0.15),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Image.asset(logoUrl, height: 120),
|
// GestureDetector for tester login
|
||||||
|
GestureDetector(
|
||||||
|
onLongPress: () {
|
||||||
|
_showTesterLoginDialog(context, loginController);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.white.withOpacity(0.5),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white.withOpacity(0.8), width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
spreadRadius: 5)
|
||||||
|
]),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
child: Image.asset('assets/images/logo.gif',
|
||||||
|
height: 100)),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white),
|
color: Colors.black87),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
subtitle,
|
subtitle,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16, color: Colors.white.withOpacity(0.8)),
|
fontSize: 16,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
GetBuilder<LoginDriverController>(builder: (context) {
|
|
||||||
return box.read(BoxName.isTest).toString() == '0'
|
|
||||||
? Container(
|
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 24, vertical: 32),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black12,
|
|
||||||
blurRadius: 12,
|
|
||||||
offset: const Offset(0, 6),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Form(
|
|
||||||
key: controller.formKey,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
// Title
|
|
||||||
|
|
||||||
Text(
|
// Glassmorphism Container for the form with a whiter look
|
||||||
'Please login to continue',
|
ClipRRect(
|
||||||
style: TextStyle(
|
borderRadius: BorderRadius.circular(25.0),
|
||||||
fontSize: 16,
|
child: BackdropFilter(
|
||||||
color: Colors.grey[600],
|
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
||||||
),
|
child: Container(
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
|
|
||||||
// Email Field
|
|
||||||
TextFormField(
|
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
controller: controller.emailController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Email'.tr,
|
|
||||||
hintText: 'Enter your email address'.tr,
|
|
||||||
prefixIcon:
|
|
||||||
const Icon(Icons.email_outlined),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
validator: (value) => value == null ||
|
|
||||||
value.isEmpty ||
|
|
||||||
!value.contains('@') ||
|
|
||||||
!value.contains('.')
|
|
||||||
? 'Enter a valid email'.tr
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Password Field
|
|
||||||
TextFormField(
|
|
||||||
obscureText: true,
|
|
||||||
controller: controller.passwordController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Password'.tr,
|
|
||||||
hintText: 'Enter your password'.tr,
|
|
||||||
prefixIcon: const Icon(Icons.lock_outline),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
validator: (value) =>
|
|
||||||
value == null || value.isEmpty
|
|
||||||
? 'Enter your password'.tr
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Submit Button
|
|
||||||
GetBuilder<LoginDriverController>(
|
|
||||||
builder: (controller) => controller.isloading
|
|
||||||
? const Center(
|
|
||||||
child: CircularProgressIndicator())
|
|
||||||
: SizedBox(
|
|
||||||
height: 50,
|
|
||||||
child: ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor:
|
|
||||||
Colors.blueAccent,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
if (controller
|
|
||||||
.formKey.currentState!
|
|
||||||
.validate()) {
|
|
||||||
controller.logintest(
|
|
||||||
controller
|
|
||||||
.passwordController.text
|
|
||||||
.trim(),
|
|
||||||
controller
|
|
||||||
.emailController.text
|
|
||||||
.trim(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Text(
|
|
||||||
'Login'.tr,
|
|
||||||
style:
|
|
||||||
const TextStyle(fontSize: 16),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Optional: Forgot Password / Create Account
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Card(
|
|
||||||
elevation: 8,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.6),
|
||||||
|
borderRadius: BorderRadius.circular(25.0),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white.withOpacity(0.8),
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
child: form,
|
child: form,
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
})
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Button for app testers, adapted to the light theme
|
||||||
|
Material(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () =>
|
||||||
|
_showTesterLoginDialog(context, loginController),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10, horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.admin_panel_settings_outlined,
|
||||||
|
color: Colors.black.withOpacity(0.6)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'For App Reviewers / Testers',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.black.withOpacity(0.6),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,7 +306,8 @@ class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
|
|||||||
final rawPhone = _phoneController.text.trim().replaceFirst('+', '');
|
final rawPhone = _phoneController.text.trim().replaceFirst('+', '');
|
||||||
final success = await PhoneAuthHelper.sendOtp(rawPhone);
|
final success = await PhoneAuthHelper.sendOtp(rawPhone);
|
||||||
if (success && mounted) {
|
if (success && mounted) {
|
||||||
Get.to(() => OtpVerificationScreen(phoneNumber: rawPhone));
|
// Get.to(() => OtpVerificationScreen(phoneNumber: rawPhone));
|
||||||
|
await PhoneAuthHelper.verifyOtp(rawPhone);
|
||||||
}
|
}
|
||||||
if (mounted) setState(() => _isLoading = false);
|
if (mounted) setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
@@ -233,75 +323,76 @@ class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// TextFormField(
|
Text(
|
||||||
// controller: _phoneController,
|
'Enter your phone number'.tr,
|
||||||
// decoration: InputDecoration(
|
style: TextStyle(color: Colors.black87, fontSize: 16),
|
||||||
// labelText: 'phone number label'.tr,
|
textAlign: TextAlign.center,
|
||||||
// prefixIcon:
|
),
|
||||||
// Icon(Icons.phone_android, color: Colors.teal.shade400),
|
const SizedBox(height: 20),
|
||||||
// border:
|
IntlPhoneField(
|
||||||
// OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
// ),
|
|
||||||
// keyboardType: TextInputType.phone,
|
|
||||||
// validator: (v) => v!.isEmpty ? 'phone number required'.tr : null,
|
|
||||||
// ),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: IntlPhoneField(
|
|
||||||
// languageCode: 'ar',
|
|
||||||
showCountryFlag: false,
|
showCountryFlag: false,
|
||||||
flagsButtonMargin: EdgeInsets.only(right: 8),
|
searchText: 'Search country'.tr,
|
||||||
flagsButtonPadding: EdgeInsets.all(4),
|
languageCode: 'ar',
|
||||||
dropdownDecoration:
|
style: const TextStyle(color: Colors.black),
|
||||||
BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
dropdownTextStyle: const TextStyle(color: Colors.black87),
|
||||||
// controller: _phoneController,
|
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Phone Number'.tr,
|
labelText: 'Phone Number'.tr,
|
||||||
border: const OutlineInputBorder(
|
hintText: 'witout zero'.tr,
|
||||||
borderSide: BorderSide(),
|
labelStyle: const TextStyle(color: Colors.black54),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.black.withOpacity(0.1)),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide:
|
||||||
|
const BorderSide(color: AppColor.greenColor, width: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
initialCountryCode: 'SY',
|
initialCountryCode: 'SY',
|
||||||
onChanged: (phone) {
|
onChanged: (phone) {
|
||||||
// Properly concatenate country code and number
|
_phoneController.text = phone.completeNumber;
|
||||||
_phoneController.text = phone.completeNumber.toString();
|
|
||||||
Log.print(' phone.number: ${phone.number}');
|
|
||||||
print("Formatted phone number: ${_phoneController.text}");
|
|
||||||
},
|
},
|
||||||
validator: (phone) {
|
validator: (phone) {
|
||||||
// Check if the phone number is not null and is valid
|
if (phone == null || phone.number.isEmpty) {
|
||||||
if (phone == null || phone.completeNumber.isEmpty) {
|
return 'Please enter your phone number'.tr;
|
||||||
return 'Please enter your phone number';
|
} // Check if the number is a Syrian number
|
||||||
|
if (phone.countryISOCode != 'SY') {
|
||||||
|
return 'Only Syrian phone numbers are allowed'.tr;
|
||||||
}
|
}
|
||||||
|
// Check if the national number part starts with '0'
|
||||||
// Extract the phone number (excluding the country code)
|
if (phone.completeNumber.startsWith('96309') ||
|
||||||
final number = phone.completeNumber.toString();
|
phone.completeNumber.startsWith('+9630') ||
|
||||||
|
phone.completeNumber.startsWith('09')) {
|
||||||
// Check if the number length is exactly 11 digits
|
return 'Please enter the number without the leading 0'.tr;
|
||||||
if (number.length != 13) {
|
}
|
||||||
return 'Phone number must be exactly 11 digits long';
|
if (phone.completeNumber.length < 10) {
|
||||||
|
return 'Phone number seems too short'.tr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If all validations pass, return null
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_isLoading
|
_isLoading
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator(color: AppColor.greenColor)
|
||||||
: ElevatedButton(
|
: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: _submit,
|
onPressed: _submit,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.teal,
|
backgroundColor: AppColor.greenColor,
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
minimumSize: const Size(double.infinity, 50),
|
|
||||||
),
|
),
|
||||||
child: Text('send otp button'.tr),
|
child: Text(
|
||||||
|
'send otp button'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -319,61 +410,17 @@ class OtpVerificationScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final TextEditingController _otpControllers = TextEditingController();
|
final _otpController = TextEditingController();
|
||||||
// final List<FocusNode> _focusNodes = List.generate(5, (_) => FocusNode());
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
// for (var controller in _otpControllers) {
|
|
||||||
// controller.dispose();
|
|
||||||
// }
|
|
||||||
// for (var node in _focusNodes) {
|
|
||||||
// node.dispose();
|
|
||||||
// }
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _submit() async {
|
void _submit() async {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
final otp = _otpControllers.text;
|
await PhoneAuthHelper.verifyOtp(widget.phoneNumber);
|
||||||
await PhoneAuthHelper.verifyOtp(widget.phoneNumber, otp);
|
|
||||||
if (mounted) setState(() => _isLoading = false);
|
if (mounted) setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOtpInput() {
|
|
||||||
return Form(
|
|
||||||
key: _formKey,
|
|
||||||
child: Container(
|
|
||||||
width: 200,
|
|
||||||
height: 50,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade200,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: TextFormField(
|
|
||||||
controller: _otpControllers,
|
|
||||||
// focusNode: _focusNodes[index],
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
maxLength: 5,
|
|
||||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
counterText: "",
|
|
||||||
border: InputBorder.none,
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
onChanged: (value) {},
|
|
||||||
validator: (v) => v!.isEmpty ? '' : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AuthScreen(
|
return AuthScreen(
|
||||||
@@ -383,20 +430,59 @@ class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
|
|||||||
form: Column(
|
form: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildOtpInput(),
|
const Text(
|
||||||
|
'Enter the 5-digit code',
|
||||||
|
style: TextStyle(color: Colors.black87, fontSize: 16),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _otpController,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
maxLength: 5,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
letterSpacing: 18,
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
counterText: "",
|
||||||
|
hintText: '-----',
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: Colors.black.withOpacity(0.2),
|
||||||
|
letterSpacing: 18,
|
||||||
|
fontSize: 28),
|
||||||
|
border: InputBorder.none,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 10),
|
||||||
|
),
|
||||||
|
validator: (v) => v == null || v.length < 5 ? '' : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
_isLoading
|
_isLoading
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator(color: AppColor.greenColor)
|
||||||
: ElevatedButton(
|
: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: _submit,
|
onPressed: _submit,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.teal,
|
backgroundColor: AppColor.greenColor,
|
||||||
foregroundColor: Colors.white,
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
minimumSize: const Size(double.infinity, 50),
|
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
child: Text('verify and continue button'.tr),
|
child: Text(
|
||||||
|
'verify and continue button'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -431,6 +517,32 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTextFormField({
|
||||||
|
required TextEditingController controller,
|
||||||
|
required String label,
|
||||||
|
TextInputType keyboardType = TextInputType.text,
|
||||||
|
String? Function(String?)? validator,
|
||||||
|
}) {
|
||||||
|
return TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
style: const TextStyle(color: Colors.black87),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
labelStyle: const TextStyle(color: Colors.black54),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: Colors.black.withOpacity(0.1)),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: const BorderSide(color: AppColor.greenColor, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
validator: validator,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AuthScreen(
|
return AuthScreen(
|
||||||
@@ -441,45 +553,44 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
_buildTextFormField(
|
||||||
controller: _firstNameController,
|
controller: _firstNameController,
|
||||||
decoration: InputDecoration(
|
label: 'first name label'.tr,
|
||||||
labelText: 'first name label'.tr,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)))),
|
|
||||||
validator: (v) => v!.isEmpty ? 'first name required'.tr : null,
|
validator: (v) => v!.isEmpty ? 'first name required'.tr : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
_buildTextFormField(
|
||||||
controller: _lastNameController,
|
controller: _lastNameController,
|
||||||
decoration: InputDecoration(
|
label: 'last name label'.tr,
|
||||||
labelText: 'last name label'.tr,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)))),
|
|
||||||
validator: (v) => v!.isEmpty ? 'last name required'.tr : null,
|
validator: (v) => v!.isEmpty ? 'last name required'.tr : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
_buildTextFormField(
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
decoration: InputDecoration(
|
label: 'email optional label'.tr,
|
||||||
labelText: 'email optional label'.tr,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)))),
|
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_isLoading
|
_isLoading
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator(color: AppColor.greenColor)
|
||||||
: ElevatedButton(
|
: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
onPressed: _submit,
|
onPressed: _submit,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.teal,
|
backgroundColor: AppColor.greenColor,
|
||||||
foregroundColor: Colors.white,
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
minimumSize: const Size(double.infinity, 50),
|
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
child: Text('complete registration button'.tr),
|
child: Text(
|
||||||
|
'complete registration button'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -121,6 +121,29 @@ class RegistrationView extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: c.bithdateController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'سنة الميلاد'.tr,
|
||||||
|
hintText: '1999'.tr,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (v) {
|
||||||
|
if (v == null || v.isEmpty) {
|
||||||
|
return 'Required field'.tr;
|
||||||
|
}
|
||||||
|
if (v.length != 4) {
|
||||||
|
return 'Birth year must be 4 digits'.tr;
|
||||||
|
}
|
||||||
|
// Optional: check if it’s a valid number
|
||||||
|
if (int.tryParse(v) == null) {
|
||||||
|
return 'Enter a valid year'.tr;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: c.driverLicenseExpiryController,
|
controller: c.driverLicenseExpiryController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@@ -163,6 +186,52 @@ class RegistrationView extends StatelessWidget {
|
|||||||
style:
|
style:
|
||||||
const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
// ============================================================
|
||||||
|
// 1. القائمة المنسدلة لتصنيف المركبة (سيارة، دراجة، فان)
|
||||||
|
// ============================================================
|
||||||
|
DropdownButtonFormField<int>(
|
||||||
|
value: c.selectedVehicleCategoryId,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Vehicle Category'.tr, // ترجمة: تصنيف المركبة
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.directions_car),
|
||||||
|
),
|
||||||
|
items: c.vehicleCategoryOptions.map((item) {
|
||||||
|
return DropdownMenuItem<int>(
|
||||||
|
value: item['id'],
|
||||||
|
child: Text(item['name']),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (val) {
|
||||||
|
c.selectedVehicleCategoryId = val;
|
||||||
|
c.update(); // تحديث الواجهة إذا لزم الأمر
|
||||||
|
},
|
||||||
|
validator: (v) => (v == null) ? 'Required field'.tr : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// ============================================================
|
||||||
|
// 2. القائمة المنسدلة لنوع الوقود (بنزين، كهرباء...)
|
||||||
|
// ============================================================
|
||||||
|
DropdownButtonFormField<int>(
|
||||||
|
value: c.selectedFuelTypeId,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Fuel Type'.tr, // ترجمة: نوع الوقود
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefixIcon: const Icon(Icons.local_gas_station),
|
||||||
|
),
|
||||||
|
items: c.fuelTypeOptions.map((item) {
|
||||||
|
return DropdownMenuItem<int>(
|
||||||
|
value: item['id'],
|
||||||
|
child: Text(item['name']),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (val) {
|
||||||
|
c.selectedFuelTypeId = val;
|
||||||
|
c.update();
|
||||||
|
},
|
||||||
|
validator: (v) => (v == null) ? 'Required field'.tr : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: c.carPlateController,
|
controller: c.carPlateController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
|||||||
@@ -39,18 +39,18 @@ class SettingsCaptain extends StatelessWidget {
|
|||||||
subtitle: 'Change the app language'.tr,
|
subtitle: 'Change the app language'.tr,
|
||||||
onTap: () => Get.to(() => const Language()),
|
onTap: () => Get.to(() => const Language()),
|
||||||
),
|
),
|
||||||
_buildListTile(
|
// _buildListTile(
|
||||||
icon: Icons.flag_outlined,
|
// icon: Icons.flag_outlined,
|
||||||
title: 'Change Country'.tr,
|
// title: 'Change Country'.tr,
|
||||||
subtitle: 'Get features for your country'.tr,
|
// subtitle: 'Get features for your country'.tr,
|
||||||
onTap: () => Get.to(
|
// onTap: () => Get.to(
|
||||||
() => MyScafolld(
|
// () => MyScafolld(
|
||||||
title: 'Change Country'.tr,
|
// title: 'Change Country'.tr,
|
||||||
body: [CountryPickerFromSetting()],
|
// body: [CountryPickerFromSetting()],
|
||||||
isleading: true,
|
// isleading: true,
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'package:sefer_driver/constant/style.dart';
|
import 'dart:ui';
|
||||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sefer_driver/constant/box_name.dart';
|
||||||
|
import 'package:sefer_driver/constant/style.dart';
|
||||||
|
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
||||||
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
|
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
|
||||||
|
|
||||||
import '../../../constant/colors.dart';
|
import '../../../constant/colors.dart';
|
||||||
import '../../../controller/functions/location_controller.dart';
|
import '../../../controller/functions/location_controller.dart';
|
||||||
|
import '../../../main.dart';
|
||||||
import '../../Rate/rate_passenger.dart';
|
import '../../Rate/rate_passenger.dart';
|
||||||
import '../../widgets/my_textField.dart';
|
import '../../widgets/my_textField.dart';
|
||||||
import 'mapDriverWidgets/driver_end_ride_bar.dart';
|
import 'mapDriverWidgets/driver_end_ride_bar.dart';
|
||||||
@@ -13,243 +15,434 @@ import 'mapDriverWidgets/google_driver_map_page.dart';
|
|||||||
import 'mapDriverWidgets/google_map_app.dart';
|
import 'mapDriverWidgets/google_map_app.dart';
|
||||||
import 'mapDriverWidgets/passenger_info_window.dart';
|
import 'mapDriverWidgets/passenger_info_window.dart';
|
||||||
import 'mapDriverWidgets/sos_connect.dart';
|
import 'mapDriverWidgets/sos_connect.dart';
|
||||||
|
import 'mapDriverWidgets/sped_circle.dart';
|
||||||
|
|
||||||
// Changed: تم إعادة بناء الصفحة بالكامل لتكون أكثر تنظيمًا
|
|
||||||
class PassengerLocationMapPage extends StatelessWidget {
|
class PassengerLocationMapPage extends StatelessWidget {
|
||||||
PassengerLocationMapPage({super.key});
|
PassengerLocationMapPage({super.key});
|
||||||
final LocationController locationController = Get.put(LocationController());
|
final LocationController locationController = Get.put(LocationController());
|
||||||
final MapDriverController mapDriverController =
|
final MapDriverController mapDriverController =
|
||||||
Get.put(MapDriverController());
|
Get.put(MapDriverController());
|
||||||
|
|
||||||
|
// دالة ديالوج الخروج
|
||||||
|
Future<bool> showExitDialog() async {
|
||||||
|
bool? result = await Get.defaultDialog(
|
||||||
|
title: "Warning".tr,
|
||||||
|
titleStyle: AppStyle.title.copyWith(color: AppColor.redColor),
|
||||||
|
middleText:
|
||||||
|
"Active ride in progress. Leaving might stop tracking. Exit?".tr,
|
||||||
|
barrierDismissible: false,
|
||||||
|
radius: 15,
|
||||||
|
confirm: MyElevatedButton(
|
||||||
|
title: 'Stay'.tr,
|
||||||
|
kolor: AppColor.greenColor,
|
||||||
|
onPressed: () => Get.back(result: false)),
|
||||||
|
cancel: MyElevatedButton(
|
||||||
|
title: 'Exit'.tr,
|
||||||
|
kolor: AppColor.redColor,
|
||||||
|
onPressed: () => Get.back(result: true)),
|
||||||
|
);
|
||||||
|
return result ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// New: استخدام addPostFrameCallback لضمان أن تحميل البيانات يتم بعد بناء الواجهة
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (Get.arguments != null && Get.arguments is Map<String, dynamic>) {
|
if (Get.arguments != null && Get.arguments is Map<String, dynamic>) {
|
||||||
mapDriverController.argumentLoading();
|
mapDriverController.argumentLoading();
|
||||||
mapDriverController.startTimerToShowPassengerInfoWindowFromDriver();
|
mapDriverController.startTimerToShowPassengerInfoWindowFromDriver();
|
||||||
} else {
|
// 2. فرض التحديث لكل المعرفات (IDs) لضمان ظهورها
|
||||||
// في حال عدم وجود arguments، يتم التعامل مع هذا الخطأ
|
// لأن argumentLoading قد تستدعي update() العادية التي لا تؤثر على هؤلاء
|
||||||
Get.snackbar("Error", "No order data found.");
|
mapDriverController
|
||||||
Get.back();
|
.update(['PassengerInfo', 'DriverEndBar', 'SosConnect']);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Scaffold(
|
return PopScope(
|
||||||
body: SafeArea(
|
canPop: false,
|
||||||
|
onPopInvokedWithResult: (didPop, result) async {
|
||||||
|
if (didPop) return;
|
||||||
|
final shouldExit = await showExitDialog();
|
||||||
|
if (shouldExit) Get.back();
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
resizeToAvoidBottomInset: false,
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
// 1. الخريطة (الخلفية)
|
||||||
|
Positioned.fill(
|
||||||
|
child: GoogleDriverMap(locationController: locationController)),
|
||||||
|
|
||||||
|
// 2. واجهة المستخدم (فوق الخريطة)
|
||||||
|
SafeArea(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// 1. الخريطة في الخلفية
|
// أ) زر الإلغاء (أعلى اليسار)
|
||||||
GoogleDriverMap(locationController: locationController),
|
CancelWidget(mapDriverController: mapDriverController),
|
||||||
|
|
||||||
// 2. شريط تعليمات الطريق في الأعلى
|
// ب) شريط إنهاء الرحلة (أعلى الوسط)
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: SafeArea(child: driverEndRideBar())),
|
||||||
|
|
||||||
|
// ج) شريط التعليمات الملاحية (الأسفل)
|
||||||
const InstructionsOfRoads(),
|
const InstructionsOfRoads(),
|
||||||
|
|
||||||
// 4. نافذة معلومات الراكب في الأسفل (تظهر قبل بدء الرحلة)
|
// د) نافذة معلومات الراكب (تعلو التعليمات ديناميكياً)
|
||||||
|
|
||||||
const PassengerInfoWindow(),
|
const PassengerInfoWindow(),
|
||||||
// 3. زر إلغاء الرحلة في الأعلى يسارًا
|
// SpeedCircle(),
|
||||||
|
Positioned(
|
||||||
|
right: 16,
|
||||||
|
bottom: 20, // أو أي مسافة تناسبك
|
||||||
|
child: GetBuilder<MapDriverController>(
|
||||||
|
// id: 'SosConnect', // لتحديث الزر عند بدء الرحلة
|
||||||
|
builder: (controller) {
|
||||||
|
// حساب الهوامش ديناميكياً لرفع الأزرار فوق النوافذ السفلية
|
||||||
|
double bottomPadding = 0;
|
||||||
|
if (controller.currentInstruction.isNotEmpty)
|
||||||
|
bottomPadding += 120;
|
||||||
|
if (controller.isPassengerInfoWindow)
|
||||||
|
bottomPadding += 220;
|
||||||
|
|
||||||
CancelWidget(mapDriverController: mapDriverController),
|
return AnimatedContainer(
|
||||||
// Changed: تم تعديل تصميم زر الإلغاء ليكون أيقونة بسيطة في الأعلى
|
duration: const Duration(milliseconds: 300),
|
||||||
// 5. شريط معلومات وإنهاء الرحلة (يظهر بعد بدء الرحلة)
|
margin: EdgeInsets.only(bottom: bottomPadding),
|
||||||
driverEndRideBar(),
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
SosConnect(), // ويدجت نظيفة
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const GoogleMapApp(), // ويدجت نظيفة
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// 6. أزرار الطوارئ والاتصال
|
// 3. النوافذ المنبثقة (Overlay)
|
||||||
const SosConnect(),
|
|
||||||
|
|
||||||
// 7. دائرة عرض السرعة
|
|
||||||
speedCircle(),
|
|
||||||
GoogleMapApp(),
|
|
||||||
// 8. نافذة عرض السعر النهائي (تظهر بعد انتهاء الرحلة)
|
|
||||||
const PricesWindow(),
|
const PricesWindow(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New: تصميم جديد لشريط تعليمات الطريق في أعلى الشاشة
|
// ---------------------------------------------------------------------------
|
||||||
|
// 1. ويدجت شريط التعليمات (InstructionsOfRoads)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
class InstructionsOfRoads extends StatelessWidget {
|
class InstructionsOfRoads extends StatelessWidget {
|
||||||
const InstructionsOfRoads({super.key});
|
const InstructionsOfRoads({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapDriverController>(
|
return Positioned(
|
||||||
builder: (controller) =>
|
bottom: 20,
|
||||||
// يتم إظهار التعليمات فقط إذا كانت متوفرة
|
left: 15,
|
||||||
controller.currentInstruction.isNotEmpty
|
right: 15,
|
||||||
? Positioned(
|
child: GetBuilder<MapDriverController>(
|
||||||
bottom: 10,
|
builder: (controller) {
|
||||||
left: MediaQuery.of(context).size.width * 0.15,
|
// إخفاء الشريط إذا لم يكن هناك تعليمات
|
||||||
right: MediaQuery.of(context).size.width * 0.15,
|
if (controller.currentInstruction.isEmpty) return const SizedBox();
|
||||||
child: AnimatedContainer(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
return TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween(begin: 0.0, end: 1.0),
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
builder: (context, value, child) {
|
||||||
|
return Transform.translate(
|
||||||
|
offset: Offset(0, 50 * (1 - value)), // حركة انزلاق
|
||||||
|
child: Opacity(
|
||||||
|
opacity: value,
|
||||||
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16, vertical: 12),
|
horizontal: 16, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: const Color(0xFF1F1F1F)
|
||||||
borderRadius: BorderRadius.circular(30),
|
.withOpacity(0.95), // خلفية داكنة
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.2),
|
color: Colors.black.withOpacity(0.4),
|
||||||
blurRadius: 8,
|
blurRadius: 15,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 5)),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
|
border: Border.all(color: Colors.white.withOpacity(0.1)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.directions,
|
// أيقونة الاتجاه
|
||||||
color: AppColor.primaryColor),
|
Container(
|
||||||
const SizedBox(width: 10),
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.primaryColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.turn_right_rounded,
|
||||||
|
color: Colors.white, size: 24),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
|
||||||
|
// نص التعليمات
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"NEXT STEP".tr,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
letterSpacing: 1.2),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
controller.currentInstruction,
|
controller.currentInstruction,
|
||||||
style: AppStyle.title.copyWith(fontSize: 16),
|
style: const TextStyle(
|
||||||
textAlign: TextAlign.center,
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
height: 1.2),
|
||||||
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// فاصل عمودي
|
||||||
|
Container(
|
||||||
|
width: 1,
|
||||||
|
height: 30,
|
||||||
|
color: Colors.white12,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 10)),
|
||||||
|
|
||||||
|
// زر التحكم بالصوت
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => controller.toggleTts(),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Icon(
|
||||||
|
controller.isTtsEnabled
|
||||||
|
? Icons.volume_up_rounded
|
||||||
|
: Icons.volume_off_rounded,
|
||||||
|
color: controller.isTtsEnabled
|
||||||
|
? AppColor.greenColor
|
||||||
|
: Colors.grey,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: const SizedBox(), // في حالة عدم وجود تعليمات، لا يظهر شيء
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changed: تم تعديل تصميم وموضع زر الإلغاء ليكون أيقونة بسيطة في الأعلى
|
// ---------------------------------------------------------------------------
|
||||||
|
// 2. ويدجت زر الإلغاء (CancelWidget) - كامل
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
class CancelWidget extends StatelessWidget {
|
class CancelWidget extends StatelessWidget {
|
||||||
const CancelWidget({
|
const CancelWidget({super.key, required this.mapDriverController});
|
||||||
super.key,
|
|
||||||
required this.mapDriverController,
|
|
||||||
});
|
|
||||||
|
|
||||||
final MapDriverController mapDriverController;
|
final MapDriverController mapDriverController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: 70,
|
top: 10,
|
||||||
left: 10,
|
left: 15,
|
||||||
child: GetBuilder<MapDriverController>(
|
child: GetBuilder<MapDriverController>(builder: (controller) {
|
||||||
builder: (controller) {
|
// نخفي الزر إذا انتهت الرحلة
|
||||||
// يظهر زر الإلغاء فقط قبل انتهاء الرحلة
|
if (controller.isRideFinished) return const SizedBox();
|
||||||
if (controller.isRideFinished) return const SizedBox.shrink();
|
|
||||||
|
|
||||||
return GestureDetector(
|
return ClipRRect(
|
||||||
onTap: () {
|
borderRadius: BorderRadius.circular(30),
|
||||||
Get.defaultDialog(
|
child: BackdropFilter(
|
||||||
title: "Are you sure you want to cancel this trip?".tr,
|
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||||
titleStyle: AppStyle.title,
|
|
||||||
content: Column(
|
|
||||||
children: [
|
|
||||||
Text("Why do you want to cancel this trip?".tr),
|
|
||||||
Form(
|
|
||||||
key: mapDriverController.formKeyCancel,
|
|
||||||
child: MyTextForm(
|
|
||||||
controller: mapDriverController.cancelTripCotroller,
|
|
||||||
label: "Write the reason for canceling the trip".tr,
|
|
||||||
hint: "Write the reason for canceling the trip".tr,
|
|
||||||
type: TextInputType.name,
|
|
||||||
))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
confirm: MyElevatedButton(
|
|
||||||
title: 'Ok'.tr,
|
|
||||||
kolor: AppColor.redColor,
|
|
||||||
onPressed: () async {
|
|
||||||
await mapDriverController
|
|
||||||
.cancelTripFromDriverAfterApplied();
|
|
||||||
Get.back();
|
|
||||||
}),
|
|
||||||
cancel: MyElevatedButton(
|
|
||||||
title: 'No'.tr,
|
|
||||||
onPressed: () {
|
|
||||||
Get.back();
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white.withOpacity(0.9),
|
||||||
shape: BoxShape.circle,
|
borderRadius: BorderRadius.circular(30),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 8)
|
||||||
color: Colors.black.withOpacity(0.2),
|
|
||||||
blurRadius: 5,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
onTap: () => _showCancelDialog(context, controller),
|
||||||
child: const Padding(
|
child: const Padding(
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.all(10.0),
|
||||||
child: Icon(
|
child: Icon(Icons.close_rounded,
|
||||||
Icons.clear,
|
color: AppColor.redColor, size: 26),
|
||||||
size: 30,
|
),
|
||||||
color: AppColor.redColor,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCancelDialog(BuildContext context, MapDriverController controller) {
|
||||||
|
Get.defaultDialog(
|
||||||
|
title: "Cancel Trip?".tr,
|
||||||
|
titleStyle: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
radius: 16,
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.warning_amber_rounded,
|
||||||
|
size: 50, color: Colors.orangeAccent),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
"Please tell us why you want to cancel.".tr,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
Form(
|
||||||
|
key: controller.formKeyCancel,
|
||||||
|
child: MyTextForm(
|
||||||
|
controller: controller.cancelTripCotroller,
|
||||||
|
label: "Reason".tr,
|
||||||
|
hint: "Write your reason...".tr,
|
||||||
|
type: TextInputType.text,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
confirm: SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: MyElevatedButton(
|
||||||
|
title: 'Confirm'.tr,
|
||||||
|
kolor: AppColor.redColor,
|
||||||
|
onPressed: () async {
|
||||||
|
// استدعاء دالة الإلغاء من الكنترولر
|
||||||
|
await controller.cancelTripFromDriverAfterApplied();
|
||||||
|
// Get.back(); // عادة موجودة داخل الدالة في الكنترولر
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
cancel: SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
child: Text('Back'.tr, style: const TextStyle(color: Colors.grey)),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Changed: تم تعديل تصميم نافذة السعر لتكون أكثر وضوحًا
|
// ---------------------------------------------------------------------------
|
||||||
|
// 3. ويدجت نافذة الأسعار (PricesWindow) - كامل
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
class PricesWindow extends StatelessWidget {
|
class PricesWindow extends StatelessWidget {
|
||||||
const PricesWindow({
|
const PricesWindow({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapDriverController>(builder: (mapDriverController) {
|
return GetBuilder<MapDriverController>(builder: (controller) {
|
||||||
return mapDriverController.isPriceWindow
|
// إخفاء إذا لم تكن مفعلة
|
||||||
? Container(
|
if (!controller.isPriceWindow) return const SizedBox();
|
||||||
color: Colors.black.withOpacity(0.5),
|
|
||||||
|
return Container(
|
||||||
|
color: Colors.black.withOpacity(0.6), // خلفية معتمة
|
||||||
child: Center(
|
child: Center(
|
||||||
|
child: TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween(begin: 0.8, end: 1.0),
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.elasticOut,
|
||||||
|
builder: (context, scale, child) {
|
||||||
|
return Transform.scale(
|
||||||
|
scale: scale,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: Get.width * 0.8,
|
width: Get.width * 0.85,
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(30),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.2),
|
||||||
|
blurRadius: 20,
|
||||||
|
spreadRadius: 5,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.primaryColor.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(Icons.check_circle_rounded,
|
||||||
|
color: AppColor.primaryColor, size: 50),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
Text(
|
Text(
|
||||||
'Total Price is '.tr,
|
'Total Price'.tr,
|
||||||
style: AppStyle.headTitle2,
|
style: AppStyle.headTitle2
|
||||||
textAlign: TextAlign.center,
|
.copyWith(fontSize: 18, color: Colors.grey[600]),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
'${mapDriverController.totalPricePassenger} ${'\$'.tr}',
|
'${controller.totalCost} ${'\$'.tr}',
|
||||||
style: AppStyle.headTitle2.copyWith(
|
style: AppStyle.headTitle2.copyWith(
|
||||||
color: AppColor.primaryColor, fontSize: 36),
|
color: Colors.black87,
|
||||||
|
fontSize: 42,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
),
|
),
|
||||||
const SizedBox(
|
|
||||||
height: 20,
|
|
||||||
),
|
),
|
||||||
MyElevatedButton(
|
const SizedBox(height: 30),
|
||||||
title: 'ok'.tr,
|
SizedBox(
|
||||||
onPressed: () =>
|
width: double.infinity,
|
||||||
|
height: 55,
|
||||||
|
child: MyElevatedButton(
|
||||||
|
title: 'Collect Payment'.tr,
|
||||||
|
kolor: AppColor.primaryColor,
|
||||||
|
onPressed: () {
|
||||||
|
// الذهاب لصفحة التقييم
|
||||||
Get.to(() => RatePassenger(), arguments: {
|
Get.to(() => RatePassenger(), arguments: {
|
||||||
'rideId': mapDriverController.rideId,
|
'rideId': controller.rideId,
|
||||||
'passengerId': mapDriverController.passengerId,
|
'passengerId': controller.passengerId,
|
||||||
'driverId': mapDriverController.driverId
|
'driverId': controller.driverId,
|
||||||
}))
|
'price': controller.paymentAmount,
|
||||||
|
'walletChecked': controller.walletChecked
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
: const SizedBox();
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import 'package:sefer_driver/views/home/my_wallet/walet_captain.dart';
|
|||||||
import 'package:sefer_driver/views/home/profile/profile_captain.dart';
|
import 'package:sefer_driver/views/home/profile/profile_captain.dart';
|
||||||
import 'package:sefer_driver/views/notification/notification_captain.dart';
|
import 'package:sefer_driver/views/notification/notification_captain.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import '../../../../constant/colors.dart';
|
||||||
import '../About Us/video_page.dart';
|
import '../About Us/video_page.dart';
|
||||||
import '../assurance_health_page.dart';
|
import '../assurance_health_page.dart';
|
||||||
import '../maintain_center_page.dart';
|
import '../maintain_center_page.dart';
|
||||||
@@ -227,12 +228,57 @@ class _DrawerItemTile extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- ويدجت محسنة للجزء العلوي من القائمة ---
|
// --- ويدجت محسنة للجزء العلوي من القائمة ---
|
||||||
|
// ... (الاستيرادات السابقة تبقى كما هي)
|
||||||
|
|
||||||
|
// --- تم تعديل UserHeader لإضافة التحقق من الصورة ---
|
||||||
class UserHeader extends StatelessWidget {
|
class UserHeader extends StatelessWidget {
|
||||||
UserHeader({super.key});
|
UserHeader({super.key});
|
||||||
final ImageController imageController = Get.find<ImageController>();
|
final ImageController imageController = Get.find<ImageController>();
|
||||||
final HomeCaptainController homeCaptainController =
|
final HomeCaptainController homeCaptainController =
|
||||||
Get.find<HomeCaptainController>();
|
Get.find<HomeCaptainController>();
|
||||||
|
|
||||||
|
// دالة لإظهار التنبيه
|
||||||
|
void _showUploadPhotoDialog(
|
||||||
|
BuildContext context, ImageController controller) {
|
||||||
|
// نستخدم addPostFrameCallback لضمان عدم ظهور الخطأ أثناء بناء الواجهة
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// نتأكد ألا يكون هناك dialog مفتوح بالفعل لتجنب التكرار
|
||||||
|
if (Get.isDialogOpen == true) return;
|
||||||
|
|
||||||
|
Get.defaultDialog(
|
||||||
|
title: "Profile Photo Required".tr,
|
||||||
|
titleStyle:
|
||||||
|
const TextStyle(color: Colors.red, fontWeight: FontWeight.bold),
|
||||||
|
middleText:
|
||||||
|
"Please upload a clear photo of your face to be identified by passengers."
|
||||||
|
.tr,
|
||||||
|
barrierDismissible: false, // منع الإغلاق بالضغط خارج النافذة
|
||||||
|
radius: 15,
|
||||||
|
contentPadding: const EdgeInsets.all(20),
|
||||||
|
confirm: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
Get.back(); // إغلاق النافذة الحالية
|
||||||
|
// فتح الكاميرا فوراً
|
||||||
|
controller.choosImagePicture(
|
||||||
|
AppLink.uploadImagePortrate, 'portrait');
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.camera_alt, color: Colors.white),
|
||||||
|
label: Text("Take Photo Now".tr,
|
||||||
|
style: const TextStyle(color: Colors.white)),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColor
|
||||||
|
.primaryColor, // تأكد من وجود هذا اللون أو استبدله بـ Colors.blue
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cancel: TextButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
child: Text("Later".tr, style: const TextStyle(color: Colors.grey)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return UserAccountsDrawerHeader(
|
return UserAccountsDrawerHeader(
|
||||||
@@ -262,8 +308,23 @@ class UserHeader extends StatelessWidget {
|
|||||||
child: controller.isloading
|
child: controller.isloading
|
||||||
? const CircularProgressIndicator(color: Colors.white)
|
? const CircularProgressIndicator(color: Colors.white)
|
||||||
: CircleAvatar(
|
: CircleAvatar(
|
||||||
|
// محاولة تحميل الصورة
|
||||||
backgroundImage: NetworkImage(
|
backgroundImage: NetworkImage(
|
||||||
'${AppLink.server}/portrate_captain_image/${box.read(BoxName.driverID)}.jpg'),
|
'${AppLink.server}/portrate_captain_image/${box.read(BoxName.driverID)}.jpg'),
|
||||||
|
|
||||||
|
// [تعديل هام]: في حال فشل تحميل الصورة (غير موجودة)
|
||||||
|
onBackgroundImageError: (exception, stackTrace) {
|
||||||
|
// طباعة الخطأ في الكونسول للتوضيح
|
||||||
|
debugPrint(
|
||||||
|
"Profile image not found or error loading: $exception");
|
||||||
|
// استدعاء نافذة التنبيه
|
||||||
|
_showUploadPhotoDialog(context, controller);
|
||||||
|
},
|
||||||
|
|
||||||
|
// أيقونة بديلة تظهر في الخلفية إذا لم تكن الصورة موجودة
|
||||||
|
backgroundColor: Colors.grey.shade300,
|
||||||
|
child: const Icon(Icons.person,
|
||||||
|
size: 40, color: Colors.white),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|||||||
@@ -1,31 +1,34 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:bubble_head/bubble.dart';
|
||||||
|
// import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
// import 'package:flutter_map/flutter_map.dart';
|
||||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
import 'package:sefer_driver/constant/box_name.dart';
|
// import 'package:latlong2/latlong.dart' as latlng; // لإحداثيات العرض
|
||||||
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
|
|
||||||
import 'package:sefer_driver/views/notification/available_rides_page.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||||
import 'package:sefer_driver/views/home/Captin/home_captain/drawer_captain.dart';
|
import 'package:sefer_driver/views/home/Captin/home_captain/drawer_captain.dart';
|
||||||
import 'package:sefer_driver/views/widgets/mycircular.dart';
|
import 'package:sefer_driver/views/widgets/mycircular.dart';
|
||||||
import 'package:bubble_head/bubble.dart';
|
|
||||||
|
|
||||||
|
import '../../../../constant/box_name.dart';
|
||||||
import '../../../../constant/colors.dart';
|
import '../../../../constant/colors.dart';
|
||||||
import '../../../../constant/info.dart';
|
import '../../../../constant/info.dart';
|
||||||
import '../../../../constant/style.dart';
|
import '../../../../constant/style.dart';
|
||||||
|
import '../../../../controller/functions/location_background_controller.dart';
|
||||||
import '../../../../controller/functions/location_controller.dart';
|
import '../../../../controller/functions/location_controller.dart';
|
||||||
import '../../../../controller/functions/overlay_permisssion.dart';
|
import '../../../../controller/functions/overlay_permisssion.dart';
|
||||||
import '../../../../controller/functions/package_info.dart';
|
import '../../../../controller/functions/package_info.dart';
|
||||||
import '../../../../controller/home/captin/home_captain_controller.dart';
|
import '../../../../controller/home/captin/home_captain_controller.dart';
|
||||||
import '../../../../print.dart';
|
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||||
|
import '../../../../main.dart';
|
||||||
|
import '../../../notification/available_rides_page.dart';
|
||||||
import '../../../widgets/circle_container.dart';
|
import '../../../widgets/circle_container.dart';
|
||||||
import '../driver_map_page.dart';
|
import '../driver_map_page.dart';
|
||||||
import 'widget/connect.dart';
|
import 'widget/connect.dart';
|
||||||
import 'widget/left_menu_map_captain.dart';
|
import 'widget/left_menu_map_captain.dart';
|
||||||
import '../../../../main.dart';
|
|
||||||
|
|
||||||
// ==================================================================
|
// ==================================================================
|
||||||
// Redesigned Main Widget (V3)
|
// Redesigned Main Widget (V3)
|
||||||
@@ -33,37 +36,43 @@ import '../../../../main.dart';
|
|||||||
class HomeCaptain extends StatelessWidget {
|
class HomeCaptain extends StatelessWidget {
|
||||||
HomeCaptain({super.key});
|
HomeCaptain({super.key});
|
||||||
|
|
||||||
final LocationController locationController = Get.put(LocationController());
|
final LocationController locationController =
|
||||||
|
Get.put(LocationController(), permanent: true);
|
||||||
final HomeCaptainController homeCaptainController =
|
final HomeCaptainController homeCaptainController =
|
||||||
Get.put(HomeCaptainController());
|
Get.put(HomeCaptainController());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Initial calls remain the same.
|
// Initial calls remain the same.
|
||||||
Get.put(HomeCaptainController());
|
// Get.put(HomeCaptainController());
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
||||||
closeOverlayIfFound();
|
|
||||||
checkForUpdate(context);
|
|
||||||
getPermissionOverlay();
|
|
||||||
showDriverGiftClaim(context);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
print("🔥 HomeCaptain postFrameCallback started"); // Debug
|
||||||
|
await closeOverlayIfFound();
|
||||||
|
await checkForUpdate(context);
|
||||||
|
await getPermissionOverlay();
|
||||||
|
await showDriverGiftClaim(context);
|
||||||
|
await checkForAppliedRide(context);
|
||||||
|
print("✅ postFrameCallback completed");
|
||||||
|
});
|
||||||
// The stack is now even simpler.
|
// The stack is now even simpler.
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const _HomeAppBar(),
|
appBar: const _HomeAppBar(),
|
||||||
drawer: AppDrawer(),
|
drawer: AppDrawer(),
|
||||||
body: Stack(
|
body: SafeArea(
|
||||||
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// 1. The Map View is the base layer.
|
// 1. The Map View is the base layer.
|
||||||
const _MapView(),
|
const _MapView(),
|
||||||
|
|
||||||
// 2. The new floating "Status Pod" at the bottom.
|
// 2. The new floating "Status Pod" at the bottom.
|
||||||
const _StatusPodOverlay(),
|
const _StatusPodOverlay(),
|
||||||
|
FloatingActionButtons(),
|
||||||
// This widget from the original code remains.
|
// This widget from the original code remains.
|
||||||
leftMainMenuCaptainIcons(),
|
leftMainMenuCaptainIcons(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,16 +139,32 @@ class _HomeAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_MapControlButton(
|
_MapControlButton(
|
||||||
|
iconColor: Colors.blue,
|
||||||
icon: Icons.satellite_alt,
|
icon: Icons.satellite_alt,
|
||||||
tooltip: 'Change Map Type'.tr,
|
tooltip: 'Change Map Type'.tr,
|
||||||
onPressed: homeCaptainController.changeMapType,
|
onPressed: homeCaptainController.changeMapType,
|
||||||
),
|
),
|
||||||
_MapControlButton(
|
// _MapControlButton(
|
||||||
icon: Icons.streetview_sharp,
|
// iconColor: Colors.blue,
|
||||||
tooltip: 'Toggle Traffic'.tr,
|
// icon: Icons.streetview_sharp,
|
||||||
onPressed: homeCaptainController.changeMapTraffic,
|
// tooltip: 'Toggle Traffic'.tr,
|
||||||
|
// onPressed: homeCaptainController.changeMapTraffic,
|
||||||
|
// ),
|
||||||
|
GetBuilder<HomeCaptainController>(
|
||||||
|
builder: (controller) {
|
||||||
|
return _MapControlButton(
|
||||||
|
// تغيير الأيقونة واللون بناءً على الحالة
|
||||||
|
icon: Icons.local_fire_department,
|
||||||
|
iconColor: controller.isHeatmapVisible
|
||||||
|
? Colors.orange
|
||||||
|
: Colors.grey,
|
||||||
|
tooltip: 'Show Heatmap'.tr,
|
||||||
|
onPressed: controller.toggleHeatmap,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_MapControlButton(
|
_MapControlButton(
|
||||||
|
iconColor: Colors.blue,
|
||||||
icon: Icons.my_location, // Changed for clarity
|
icon: Icons.my_location, // Changed for clarity
|
||||||
tooltip: 'Center on Me'.tr,
|
tooltip: 'Center on Me'.tr,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -188,32 +213,55 @@ class _MapView extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final locationController = Get.find<LocationController>();
|
// جلب الكونترولر الرئيسي
|
||||||
return GetBuilder<HomeCaptainController>(builder: (controller) {
|
final homeController = Get.find<HomeCaptainController>();
|
||||||
return controller.isLoading
|
|
||||||
? const MyCircularProgressIndicator()
|
return GetBuilder<HomeCaptainController>(
|
||||||
: GoogleMap(
|
builder: (controller) {
|
||||||
|
if (controller.isLoading) {
|
||||||
|
return const MyCircularProgressIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- هذا هو التعديل ---
|
||||||
|
// هذا الـ Builder يستمع إلى تحديثات الموقع
|
||||||
|
return GetBuilder<LocationController>(
|
||||||
|
builder: (locationController) {
|
||||||
|
// if (homeController.mapHomeCaptainController != null &&
|
||||||
|
// homeController.isActive) {
|
||||||
|
// homeController.mapHomeCaptainController!.animateCamera(
|
||||||
|
// CameraUpdate.newCameraPosition(
|
||||||
|
// CameraPosition(
|
||||||
|
// target: locationController.myLocation, // الموقع الجديد
|
||||||
|
// zoom: 17.5, // تقريب لمتابعة السائق
|
||||||
|
// tilt: 50.0, // زاوية رؤية 3D
|
||||||
|
// bearing: locationController.heading, // اتجاه السيارة
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// --- نهاية الكود الجديد ---
|
||||||
|
|
||||||
|
// إرجاع الخريطة
|
||||||
|
return GoogleMap(
|
||||||
padding: const EdgeInsets.only(bottom: 110, top: 300),
|
padding: const EdgeInsets.only(bottom: 110, top: 300),
|
||||||
fortyFiveDegreeImageryEnabled: true,
|
fortyFiveDegreeImageryEnabled: true,
|
||||||
onMapCreated: controller.onMapCreated,
|
onMapCreated: controller.onMapCreated,
|
||||||
|
// onCameraMove: controller.onCameraMove,
|
||||||
minMaxZoomPreference: const MinMaxZoomPreference(6, 18),
|
minMaxZoomPreference: const MinMaxZoomPreference(6, 18),
|
||||||
initialCameraPosition: CameraPosition(
|
initialCameraPosition: CameraPosition(
|
||||||
target: locationController.myLocation,
|
target: locationController.myLocation,
|
||||||
zoom: 15,
|
zoom: 15,
|
||||||
),
|
),
|
||||||
onCameraMove: (position) {
|
// --- تم حذف onCameraMove الخاطئ ---
|
||||||
CameraPosition(
|
// === إضافة الطبقة الحرارية هنا ===
|
||||||
target: locationController.myLocation,
|
polygons: controller.heatmapPolygons,
|
||||||
zoom: 17.5,
|
|
||||||
tilt: 50.0,
|
// =
|
||||||
bearing: locationController.heading,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
markers: {
|
markers: {
|
||||||
Marker(
|
Marker(
|
||||||
markerId: MarkerId('MyLocation'.tr),
|
markerId: MarkerId('MyLocation'.tr),
|
||||||
position: locationController.myLocation,
|
position: locationController.myLocation, // يتم تحديثه من هنا
|
||||||
rotation: locationController.heading,
|
rotation: locationController.heading, // يتم تحديثه من هنا
|
||||||
flat: true,
|
flat: true,
|
||||||
anchor: const Offset(0.5, 0.5),
|
anchor: const Offset(0.5, 0.5),
|
||||||
icon: controller.carIcon,
|
icon: controller.carIcon,
|
||||||
@@ -223,12 +271,80 @@ class _MapView extends StatelessWidget {
|
|||||||
myLocationButtonEnabled: false,
|
myLocationButtonEnabled: false,
|
||||||
myLocationEnabled: false,
|
myLocationEnabled: false,
|
||||||
trafficEnabled: controller.mapTrafficON,
|
trafficEnabled: controller.mapTrafficON,
|
||||||
buildingsEnabled: true,
|
buildingsEnabled: false,
|
||||||
mapToolbarEnabled: false,
|
mapToolbarEnabled: false,
|
||||||
compassEnabled: true,
|
compassEnabled: false,
|
||||||
zoomControlsEnabled: false,
|
zoomControlsEnabled: false,
|
||||||
);
|
);
|
||||||
});
|
// --- الكود الجديد ---
|
||||||
|
|
||||||
|
// // تحويل الإحداثيات من جوجل (إذا كنت لا تزال تستخدمها) إلى latlong2
|
||||||
|
// final latlng.LatLng currentCarPosition = latlng.LatLng(
|
||||||
|
// locationController.myLocation.latitude,
|
||||||
|
// locationController.myLocation.longitude);
|
||||||
|
|
||||||
|
// return FlutterMap(
|
||||||
|
// // 1. تمرير الـ Controller الذي أنشأناه في الخطوة 2
|
||||||
|
// mapController: homeController.mapController,
|
||||||
|
|
||||||
|
// options: MapOptions(
|
||||||
|
// // 2. هذا بديل initialCameraPosition
|
||||||
|
// initialCenter: currentCarPosition,
|
||||||
|
// initialZoom: 15,
|
||||||
|
// // هذا بديل padding
|
||||||
|
// // (ملاحظة: flutter_map لا يدعم padding مباشرة، قد تحتاج لتعديل الواجهة
|
||||||
|
// // أو استخدام خاصية nonRotatedChildren لبدائل أخرى)
|
||||||
|
|
||||||
|
// // هذا بديل minMaxZoomPreference
|
||||||
|
// minZoom: 12,
|
||||||
|
// maxZoom: 16, onMapReady: homeController.onMapReady,
|
||||||
|
// ),
|
||||||
|
|
||||||
|
// // 3. الخرائط في flutter_map عبارة عن "طبقات" (Layers)
|
||||||
|
// children: [
|
||||||
|
// // --- الطبقة الأولى: الخريطة الأساسية (OSM) ---
|
||||||
|
// TileLayer(
|
||||||
|
// urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
|
// userAgentPackageName:
|
||||||
|
// 'com.your.app.name', // هام: ضع اسم تطبيقك هنا
|
||||||
|
// ),
|
||||||
|
|
||||||
|
// // --- (اختياري) طبقة القمر الصناعي بناءً على MapType ---
|
||||||
|
// if (controller.mapType) // إذا كنت لا تزال تستخدم mapType
|
||||||
|
// // TileLayer(
|
||||||
|
// // // ملاحظة: هذا الرابط يحتاج مفتاح API من MapTiler أو مزود آخر
|
||||||
|
// // urlTemplate:
|
||||||
|
// // 'https://api.maptiler.com/maps/satellite/{z}/{x}/{y}.jpg?key=YOUR_API_KEY',
|
||||||
|
// // userAgentPackageName: 'com.your.app.name',
|
||||||
|
// // ),
|
||||||
|
|
||||||
|
// // --- الطبقة الثانية: أيقونة السيارة (Marker) ---
|
||||||
|
// MarkerLayer(
|
||||||
|
// markers: [
|
||||||
|
// Marker(
|
||||||
|
// point: currentCarPosition, // الإحداثيات
|
||||||
|
// width: 80,
|
||||||
|
// height: 80,
|
||||||
|
// child: Transform.rotate(
|
||||||
|
// // 4. هذا بديل rotation
|
||||||
|
// angle: locationController.heading *
|
||||||
|
// (3.1415926535 / 180), // تحويل من درجات إلى راديان
|
||||||
|
// // 5. هذا بديل carIcon (أصبح أسهل!)
|
||||||
|
// child: Image.asset(
|
||||||
|
// 'assets/images/car.png', // نفس المسار الذي استخدمته من قبل
|
||||||
|
// width: 30, // الحجم الذي حددته في addCustomCarIcon
|
||||||
|
// height: 35,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// );
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,9 +352,10 @@ class _MapView extends StatelessWidget {
|
|||||||
class _StatusPodOverlay extends StatelessWidget {
|
class _StatusPodOverlay extends StatelessWidget {
|
||||||
const _StatusPodOverlay();
|
const _StatusPodOverlay();
|
||||||
|
|
||||||
void _showDetailsDialog(BuildContext context) {
|
void _showDetailsDialog(
|
||||||
|
BuildContext context, HomeCaptainController controller) {
|
||||||
Get.dialog(
|
Get.dialog(
|
||||||
const _DriverDetailsDialog(),
|
_DriverDetailsDialog(controller), // تمرير الكنترولر هنا
|
||||||
barrierColor: Colors.black.withOpacity(0.3),
|
barrierColor: Colors.black.withOpacity(0.3),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -251,7 +368,7 @@ class _StatusPodOverlay extends StatelessWidget {
|
|||||||
left: 16,
|
left: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => _showDetailsDialog(context),
|
onTap: () => _showDetailsDialog(context, homeCaptainController),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
child: BackdropFilter(
|
child: BackdropFilter(
|
||||||
@@ -325,11 +442,16 @@ class _StatusPodOverlay extends StatelessWidget {
|
|||||||
|
|
||||||
/// 4. The Dialog that shows detailed driver stats.
|
/// 4. The Dialog that shows detailed driver stats.
|
||||||
class _DriverDetailsDialog extends StatelessWidget {
|
class _DriverDetailsDialog extends StatelessWidget {
|
||||||
const _DriverDetailsDialog();
|
// 1. إضافة متغير للكنترولر
|
||||||
|
final HomeCaptainController controller;
|
||||||
|
|
||||||
|
// 2. تحديث البناء لاستقباله
|
||||||
|
const _DriverDetailsDialog(this.controller);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final homeCaptainController = Get.find<HomeCaptainController>();
|
// 3. حذف السطر الذي يسبب الخطأ: final homeCaptainController = Get.find...
|
||||||
|
|
||||||
return BackdropFilter(
|
return BackdropFilter(
|
||||||
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||||
child: AlertDialog(
|
child: AlertDialog(
|
||||||
@@ -353,27 +475,28 @@ class _DriverDetailsDialog extends StatelessWidget {
|
|||||||
icon: Entypo.wallet,
|
icon: Entypo.wallet,
|
||||||
color: AppColor.greenColor,
|
color: AppColor.greenColor,
|
||||||
label: 'Today'.tr,
|
label: 'Today'.tr,
|
||||||
value: homeCaptainController.totalMoneyToday.toString(),
|
// استخدام المتغير controller الذي تم تمريره
|
||||||
|
value: controller.totalMoneyToday.toString(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildStatRow(
|
_buildStatRow(
|
||||||
icon: Entypo.wallet,
|
icon: Entypo.wallet,
|
||||||
color: AppColor.yellowColor,
|
color: AppColor.yellowColor,
|
||||||
label: AppInformation.appName,
|
label: AppInformation.appName,
|
||||||
value: homeCaptainController.totalMoneyInSEFER.toString(),
|
value: controller.totalMoneyInSEFER.toString(),
|
||||||
),
|
),
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
_buildDurationRow(
|
_buildDurationRow(
|
||||||
icon: Icons.timer_outlined,
|
icon: Icons.timer_outlined,
|
||||||
label: 'Active Duration:'.tr,
|
label: 'Active Duration:'.tr,
|
||||||
value: homeCaptainController.stringActiveDuration,
|
value: controller.stringActiveDuration,
|
||||||
color: AppColor.greenColor,
|
color: AppColor.greenColor,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildDurationRow(
|
_buildDurationRow(
|
||||||
icon: Icons.access_time,
|
icon: Icons.access_time,
|
||||||
label: 'Total Connection Duration:'.tr,
|
label: 'Total Connection Duration:'.tr,
|
||||||
value: homeCaptainController.totalDurationToday,
|
value: controller.totalDurationToday,
|
||||||
color: AppColor.accentColor,
|
color: AppColor.accentColor,
|
||||||
),
|
),
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
@@ -381,7 +504,7 @@ class _DriverDetailsDialog extends StatelessWidget {
|
|||||||
icon: Icons.star_border_rounded,
|
icon: Icons.star_border_rounded,
|
||||||
color: AppColor.blueColor,
|
color: AppColor.blueColor,
|
||||||
label: 'Total Points'.tr,
|
label: 'Total Points'.tr,
|
||||||
value: homeCaptainController.totalPoints.toString(),
|
value: controller.totalPoints.toString(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -398,6 +521,7 @@ class _DriverDetailsDialog extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... بقية الدوال المساعدة (_buildStatRow, _buildDurationRow) تبقى كما هي ...
|
||||||
Widget _buildStatRow(
|
Widget _buildStatRow(
|
||||||
{required IconData icon,
|
{required IconData icon,
|
||||||
required Color color,
|
required Color color,
|
||||||
@@ -448,6 +572,7 @@ class _MapControlButton extends StatelessWidget {
|
|||||||
required this.icon,
|
required this.icon,
|
||||||
required this.onPressed,
|
required this.onPressed,
|
||||||
required this.tooltip,
|
required this.tooltip,
|
||||||
|
required MaterialColor iconColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -473,44 +598,166 @@ class _MapControlButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// NOTE: The _FloatingActionButtons and _MapControlButton widgets have been removed
|
class FloatingActionButtons extends StatelessWidget {
|
||||||
/// as their functionality is now integrated into the _HomeAppBar.
|
const FloatingActionButtons();
|
||||||
///
|
|
||||||
/// You will still need to modify your existing `ConnectWidget`
|
|
||||||
/// to accept an `isCompact` boolean flag as mentioned in the previous design.
|
|
||||||
/*
|
|
||||||
class ConnectWidget extends StatelessWidget {
|
|
||||||
final bool isCompact;
|
|
||||||
const ConnectWidget({super.key, this.isCompact = false});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// ... your existing controller logic
|
// نفس الكود الأصلي للأزرار
|
||||||
|
return Positioned(
|
||||||
if (isCompact) {
|
bottom: Get.height * .2,
|
||||||
// Return a smaller version for the pod
|
right: 6,
|
||||||
return Container(
|
child:
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
GetBuilder<HomeCaptainController>(builder: (homeCaptainController) {
|
||||||
decoration: BoxDecoration(
|
return Column(
|
||||||
color: controller.isConnect ? AppColor.greenColor : AppColor.accentColor,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
Icon(controller.isConnect ? Icons.wifi_tethering_rounded : Icons.wifi_tethering_off_rounded, color: Colors.white, size: 20),
|
Platform.isAndroid
|
||||||
const SizedBox(width: 8),
|
? AnimatedContainer(
|
||||||
Text(
|
duration: const Duration(microseconds: 200),
|
||||||
controller.isConnect ? 'Online'.tr : 'Offline'.tr,
|
width: homeCaptainController.widthMapTypeAndTraffic,
|
||||||
style: AppStyle.title.copyWith(color: Colors.white, fontSize: 14),
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: AppColor.blueColor),
|
||||||
|
color: AppColor.secondaryColor,
|
||||||
|
borderRadius: BorderRadius.circular(15)),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Bubble().startBubbleHead(sendAppToBackground: true);
|
||||||
|
},
|
||||||
|
icon: Image.asset(
|
||||||
|
'assets/images/logo1.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: 35,
|
||||||
|
height: 35,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox(),
|
||||||
|
const SizedBox(
|
||||||
|
height: 5,
|
||||||
|
),
|
||||||
|
AnimatedContainer(
|
||||||
|
duration: const Duration(microseconds: 200),
|
||||||
|
width: homeCaptainController.widthMapTypeAndTraffic,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: AppColor.blueColor),
|
||||||
|
color: AppColor.secondaryColor,
|
||||||
|
borderRadius: BorderRadius.circular(15)),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
Get.to(() => const AvailableRidesPage());
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.train_sharp,
|
||||||
|
size: 29,
|
||||||
|
color: AppColor.blueColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 5,
|
||||||
|
),
|
||||||
|
// هذا الكود يوضع داخل الـ Stack في ملف الواجهة (HomeCaptain View)
|
||||||
|
|
||||||
|
box.read(BoxName.rideStatus) == 'Applied' ||
|
||||||
|
box.read(BoxName.rideStatus) == 'Begin'
|
||||||
|
? Positioned(
|
||||||
|
bottom: Get.height * .2,
|
||||||
|
// جعلنا الزر يظهر في المنتصف أو يمتد ليكون واضحاً جداً
|
||||||
|
right: 20,
|
||||||
|
left: 20,
|
||||||
|
child: Center(
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(
|
||||||
|
milliseconds:
|
||||||
|
200), // تم تصحيح microseconds إلى milliseconds لحركة أنعم
|
||||||
|
// أزلنا العرض الثابت homeCaptainController.widthMapTypeAndTraffic لكي يتسع للنص
|
||||||
|
// width: homeCaptainController.widthMapTypeAndTraffic,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColor.blueColor,
|
||||||
|
width: 2), // تعريض الإطار قليلاً
|
||||||
|
color: AppColor.secondaryColor, // لون الخلفية
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
30), // تدوير الحواف ليشبه الأزرار الحديثة
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.2),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
onLongPress: () {
|
||||||
|
// وظيفة الحذف عند الضغط الطويل (للطوارئ)
|
||||||
|
box.write(BoxName.rideStatus, 'delete');
|
||||||
|
homeCaptainController.update();
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
// نفس منطقك الأصلي للانتقال
|
||||||
|
if (box.read(BoxName.rideStatus) == 'Applied') {
|
||||||
|
Get.to(() => PassengerLocationMapPage(),
|
||||||
|
arguments: box.read(BoxName.rideArguments));
|
||||||
|
Get.put(MapDriverController())
|
||||||
|
.changeRideToBeginToPassenger();
|
||||||
|
} else {
|
||||||
|
Get.to(() => PassengerLocationMapPage(),
|
||||||
|
arguments: box.read(BoxName.rideArguments));
|
||||||
|
Get.put(MapDriverController())
|
||||||
|
.startRideFromStartApp();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize:
|
||||||
|
MainAxisSize.min, // حجم الزر على قد المحتوى
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons
|
||||||
|
.directions_car_filled_rounded, // تغيير الأيقونة لسيارة أو اتجاهات لتكون معبرة أكثر
|
||||||
|
size: 24,
|
||||||
|
color: AppColor.blueColor,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 10), // مسافة بين الأيقونة والنص
|
||||||
|
Text(
|
||||||
|
"متابعة الرحلة", // النص الواضح للسائق
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColor.blueColor,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontFamily:
|
||||||
|
'Cairo', // تأكد من نوع الخط المستخدم عندك
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (box.read(BoxName.rideStatus) ==
|
||||||
|
'Begin') ...[
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
// إضافة مؤشر صغير (نقطة حمراء) إذا كانت الرحلة قد بدأت بالفعل (اختياري)
|
||||||
|
const Icon(Icons.circle,
|
||||||
|
size: 8, color: Colors.green)
|
||||||
|
]
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox()
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the original, larger button
|
|
||||||
return ElevatedButton.icon(...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
Future<void> checkForAppliedRide(BuildContext context) async {
|
||||||
|
checkForPendingOrderFromServer();
|
||||||
|
}
|
||||||
|
|||||||
83
lib/views/home/Captin/home_captain/osm_view_map.dart
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
import '../../../../controller/functions/location_controller.dart';
|
||||||
|
import '../../../../controller/home/captin/home_captain_controller.dart';
|
||||||
|
// هذه ويدجت بديلة للـ _MapView في الكود الخاص بك
|
||||||
|
// V3 - MapView Replacement using flutter_map
|
||||||
|
|
||||||
|
class OsmMapView extends StatelessWidget {
|
||||||
|
const OsmMapView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// افترض أنك تحصل على الموقع والاتجاه بنفس الطريقة من الكونترولر
|
||||||
|
final LocationController locationController =
|
||||||
|
Get.find<LocationController>();
|
||||||
|
final HomeCaptainController homeCaptainController =
|
||||||
|
Get.find<HomeCaptainController>();
|
||||||
|
|
||||||
|
// يمكنك استخدام GetBuilder لمراقبة التغييرات في الموقع
|
||||||
|
return Obx(() {
|
||||||
|
final LatLng currentLocation = LatLng(
|
||||||
|
locationController.myLocation.latitude,
|
||||||
|
locationController.myLocation.longitude);
|
||||||
|
final double currentHeading = locationController.heading;
|
||||||
|
|
||||||
|
return FlutterMap(
|
||||||
|
// يمكنك ربط هذا بـ MapController الخاص بـ flutter_map
|
||||||
|
// mapController: homeCaptainController.flutterMapController,
|
||||||
|
options: MapOptions(
|
||||||
|
initialCenter: currentLocation,
|
||||||
|
initialZoom: 15,
|
||||||
|
maxZoom: 18,
|
||||||
|
minZoom: 6,
|
||||||
|
// تدوير الخريطة (اختياري)
|
||||||
|
initialRotation: currentHeading,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
// 1. طبقة الخريطة الأساسية (Tiles)
|
||||||
|
// هذا هو الرابط لخرائط OSM الأساسية
|
||||||
|
TileLayer(
|
||||||
|
urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
|
subdomains: ['a', 'b', 'c'],
|
||||||
|
userAgentPackageName:
|
||||||
|
'com.example.app', // استبدل باسم الباكج الخاص بك
|
||||||
|
|
||||||
|
// لاستخدام الخرائط الأوفلاين (بعد إعداد flutter_map_tile_caching)
|
||||||
|
// tileProvider: CachedTileProvider(),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 2. طبقة العلامات (Markers)
|
||||||
|
MarkerLayer(
|
||||||
|
markers: [
|
||||||
|
Marker(
|
||||||
|
width: 80.0,
|
||||||
|
height: 80.0,
|
||||||
|
point: currentLocation,
|
||||||
|
child: Transform.rotate(
|
||||||
|
angle: currentHeading *
|
||||||
|
(3.1415926535 / 180), // تحويل من درجات إلى راديان
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/car_icon.png', // تأكد أن لديك أيقونة السيارة
|
||||||
|
// يمكنك استخدام نفس الـ carIcon من الكونترولر
|
||||||
|
// icon: homeCaptainController.carIcon, (ملاحظة: flutter_map تستخدم ويدجت)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// يمكنك إضافة طبقات أخرى هنا (مثل الخطوط Polylines أو المضلعات Polygons)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ملاحظة: ستحتاج إلى تعديل بسيط في HomeCaptainController
|
||||||
|
// لإنشاء MapController الخاص بـ flutter_map بدلاً من GoogleMapController
|
||||||
|
// import 'package:flutter_map/flutter_map.dart';
|
||||||
|
// MapController flutterMapController = MapController();
|
||||||
@@ -20,13 +20,13 @@ class ConnectWidget extends StatelessWidget {
|
|||||||
// Get.put(OrderRequestController());
|
// Get.put(OrderRequestController());
|
||||||
CaptainWalletController captainWalletController =
|
CaptainWalletController captainWalletController =
|
||||||
Get.put(CaptainWalletController());
|
Get.put(CaptainWalletController());
|
||||||
|
int refusedRidesToday = 0;
|
||||||
captainWalletController.getCaptainWalletFromBuyPoints();
|
captainWalletController.getCaptainWalletFromBuyPoints();
|
||||||
return Center(
|
return Center(
|
||||||
child: GetBuilder<HomeCaptainController>(
|
child: GetBuilder<HomeCaptainController>(
|
||||||
builder: (homeCaptainController) => double.parse(
|
builder: (homeCaptainController) => double.parse(
|
||||||
(captainWalletController.totalPoints)) <
|
(captainWalletController.totalPoints)) <
|
||||||
-30000
|
-200
|
||||||
? CupertinoButton(
|
? CupertinoButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
@@ -34,7 +34,7 @@ class ConnectWidget extends StatelessWidget {
|
|||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
title: double.parse(
|
title: double.parse(
|
||||||
(captainWalletController.totalPoints)) <
|
(captainWalletController.totalPoints)) <
|
||||||
-30000
|
-200
|
||||||
? 'You dont have Points'.tr
|
? 'You dont have Points'.tr
|
||||||
: 'You Are Stopped For this Day !'.tr,
|
: 'You Are Stopped For this Day !'.tr,
|
||||||
titleStyle: AppStyle.title,
|
titleStyle: AppStyle.title,
|
||||||
@@ -44,7 +44,7 @@ class ConnectWidget extends StatelessWidget {
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
double.parse((captainWalletController
|
double.parse((captainWalletController
|
||||||
.totalPoints)) <
|
.totalPoints)) <
|
||||||
-30000
|
-200
|
||||||
? await Get.find<TextToSpeechController>()
|
? await Get.find<TextToSpeechController>()
|
||||||
.speakText(
|
.speakText(
|
||||||
'You must be recharge your Account'
|
'You must be recharge your Account'
|
||||||
@@ -59,7 +59,7 @@ class ConnectWidget extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
double.parse((captainWalletController
|
double.parse((captainWalletController
|
||||||
.totalPoints)) <
|
.totalPoints)) <
|
||||||
-30000
|
-200
|
||||||
? 'You must be recharge your Account'.tr
|
? 'You must be recharge your Account'.tr
|
||||||
: 'You Refused 3 Rides this Day that is the reason \nSee you Tomorrow!'
|
: 'You Refused 3 Rides this Day that is the reason \nSee you Tomorrow!'
|
||||||
.tr,
|
.tr,
|
||||||
@@ -69,7 +69,7 @@ class ConnectWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
confirm: double.parse(
|
confirm: double.parse(
|
||||||
(captainWalletController.totalPoints)) <
|
(captainWalletController.totalPoints)) <
|
||||||
-30000
|
-200
|
||||||
? MyElevatedButton(
|
? MyElevatedButton(
|
||||||
title: 'Recharge my Account'.tr,
|
title: 'Recharge my Account'.tr,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||||
import 'package:sefer_driver/constant/box_name.dart';
|
import 'package:sefer_driver/constant/box_name.dart';
|
||||||
import 'package:sefer_driver/controller/firebase/local_notification.dart';
|
import 'package:sefer_driver/controller/firebase/local_notification.dart';
|
||||||
import 'package:sefer_driver/controller/functions/network/net_guard.dart';
|
|
||||||
import 'package:sefer_driver/controller/functions/sms_egypt_controller.dart';
|
|
||||||
import 'package:sefer_driver/main.dart';
|
import 'package:sefer_driver/main.dart';
|
||||||
import 'package:sefer_driver/views/auth/captin/login_captin.dart';
|
import 'package:sefer_driver/views/auth/captin/otp_page.dart';
|
||||||
import 'package:sefer_driver/views/home/Captin/driver_map_page.dart';
|
import 'package:sefer_driver/views/home/Captin/driver_map_page.dart';
|
||||||
import 'package:sefer_driver/views/home/Captin/orderCaptin/vip_order_page.dart';
|
import 'package:sefer_driver/views/home/Captin/orderCaptin/vip_order_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -15,13 +14,12 @@ import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
|||||||
import '../../../../../constant/colors.dart';
|
import '../../../../../constant/colors.dart';
|
||||||
import '../../../../../constant/links.dart';
|
import '../../../../../constant/links.dart';
|
||||||
import '../../../../../controller/firebase/firbase_messge.dart';
|
import '../../../../../controller/firebase/firbase_messge.dart';
|
||||||
|
import '../../../../../controller/firebase/notification_service.dart';
|
||||||
import '../../../../../controller/functions/crud.dart';
|
import '../../../../../controller/functions/crud.dart';
|
||||||
import '../../../../../controller/functions/encrypt_decrypt.dart';
|
|
||||||
import '../../../../../controller/home/captin/order_request_controller.dart';
|
import '../../../../../controller/home/captin/order_request_controller.dart';
|
||||||
import '../../../../../controller/home/navigation/navigation_view.dart';
|
import '../../../../../controller/home/navigation/navigation_view.dart';
|
||||||
|
import '../../../../../print.dart';
|
||||||
import '../../../../Rate/ride_calculate_driver.dart';
|
import '../../../../Rate/ride_calculate_driver.dart';
|
||||||
import '../../../../auth/syria/registration_view.dart';
|
|
||||||
import '../../../../widgets/error_snakbar.dart';
|
|
||||||
|
|
||||||
GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
|
GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
|
||||||
final firebaseMessagesController =
|
final firebaseMessagesController =
|
||||||
@@ -105,7 +103,7 @@ GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
|
|||||||
// Get.snackbar(
|
// Get.snackbar(
|
||||||
// '${'Please wait'.tr} $minutesLeft ${"minutes before trying again.".tr}',
|
// '${'Please wait'.tr} $minutesLeft ${"minutes before trying again.".tr}',
|
||||||
// '');
|
// '');
|
||||||
NotificationController1().showNotification(
|
NotificationController().showNotification(
|
||||||
'Intaleq Driver'.tr,
|
'Intaleq Driver'.tr,
|
||||||
'${'Please wait'.tr} $minutesLeft ${"minutes before trying again.".tr}',
|
'${'Please wait'.tr} $minutesLeft ${"minutes before trying again.".tr}',
|
||||||
'ding',
|
'ding',
|
||||||
@@ -185,8 +183,13 @@ GetBuilder<HomeCaptainController> leftMainMenuCaptainIcons() {
|
|||||||
// child: Builder(builder: (context) {
|
// child: Builder(builder: (context) {
|
||||||
// return IconButton(
|
// return IconButton(
|
||||||
// onPressed: () async {
|
// onPressed: () async {
|
||||||
// var finger = await storage.read(key: BoxName.fingerPrint);
|
// NotificationService.sendNotification(
|
||||||
//
|
// target: 'service', // الإرسال لجميع المشتركين في "service"
|
||||||
|
// title: 'طلب خدمة جديد',
|
||||||
|
// body: 'تم استلام طلب خدمة جديد. الرجاء مراجعة التفاصيل.',
|
||||||
|
// isTopic: true,
|
||||||
|
// category: 'new_service_request', // فئة توضح نوع الإشعار
|
||||||
|
// );
|
||||||
// },
|
// },
|
||||||
// icon: const Icon(
|
// icon: const Icon(
|
||||||
// FontAwesome5.grin_tears,
|
// FontAwesome5.grin_tears,
|
||||||
@@ -221,9 +224,10 @@ Future<void> checkForPendingOrderFromServer() async {
|
|||||||
link: AppLink.getArgumentAfterAppliedFromBackground,
|
link: AppLink.getArgumentAfterAppliedFromBackground,
|
||||||
payload: {'driver_id': driverId},
|
payload: {'driver_id': driverId},
|
||||||
);
|
);
|
||||||
|
Log.print('response: ${response}');
|
||||||
|
|
||||||
// Assuming the server returns order data if found, or 'failure'/'none' if not
|
// Assuming the server returns order data if found, or 'failure'/'none' if not
|
||||||
if (response != 'failure') {
|
if (response['status'] == 'success') {
|
||||||
final Map<String, dynamic> orderInfoFromServer = response['message'];
|
final Map<String, dynamic> orderInfoFromServer = response['message'];
|
||||||
final Map<String, dynamic> rideArguments =
|
final Map<String, dynamic> rideArguments =
|
||||||
_transformServerDataToAppArguments(orderInfoFromServer);
|
_transformServerDataToAppArguments(orderInfoFromServer);
|
||||||
@@ -239,8 +243,10 @@ Future<void> checkForPendingOrderFromServer() async {
|
|||||||
// MyDialog().getDialog(orderId.toString(), customerToken, () {});
|
// MyDialog().getDialog(orderId.toString(), customerToken, () {});
|
||||||
|
|
||||||
// Now proceed with the UI flow
|
// Now proceed with the UI flow
|
||||||
_sendAcceptanceNotification(customerToken, orderId.toString());
|
// _sendAcceptanceNotification(customerToken, orderId.toString());
|
||||||
// await _bringAppToForegroundAndNavigate(orderId);
|
// await _bringAppToForegroundAndNavigate(orderId);
|
||||||
|
Get.to(() => PassengerLocationMapPage(),
|
||||||
|
arguments: box.read(BoxName.rideArgumentsFromBackground));
|
||||||
} else {
|
} else {
|
||||||
box.write(BoxName.rideArgumentsFromBackground, 'failure');
|
box.write(BoxName.rideArgumentsFromBackground, 'failure');
|
||||||
}
|
}
|
||||||
@@ -309,8 +315,7 @@ Map<String, dynamic> _transformServerDataToAppArguments(
|
|||||||
void _sendAcceptanceNotification(String? customerToken, rideId) {
|
void _sendAcceptanceNotification(String? customerToken, rideId) {
|
||||||
try {
|
try {
|
||||||
if (customerToken == null) return;
|
if (customerToken == null) return;
|
||||||
final FirebaseMessagesController _firebaseMessagesController =
|
|
||||||
Get.put(FirebaseMessagesController());
|
|
||||||
List<String> bodyToPassenger = [
|
List<String> bodyToPassenger = [
|
||||||
box.read(BoxName.driverID).toString(),
|
box.read(BoxName.driverID).toString(),
|
||||||
box.read(BoxName.nameDriver).toString(),
|
box.read(BoxName.nameDriver).toString(),
|
||||||
@@ -321,8 +326,14 @@ void _sendAcceptanceNotification(String? customerToken, rideId) {
|
|||||||
// Safely check for customer token
|
// Safely check for customer token
|
||||||
final String? token = customerToken;
|
final String? token = customerToken;
|
||||||
if (token != null && token.isNotEmpty) {
|
if (token != null && token.isNotEmpty) {
|
||||||
_firebaseMessagesController.sendNotificationToDriverMAP('Accepted Ride',
|
NotificationService.sendNotification(
|
||||||
'your ride is applied'.tr, token, bodyToPassenger, 'start.wav');
|
target: token.toString(),
|
||||||
|
title: 'Accepted Ride'.tr,
|
||||||
|
body: 'your ride is Accepted'.tr,
|
||||||
|
isTopic: false, // Important: this is a token
|
||||||
|
tone: 'start',
|
||||||
|
driverList: bodyToPassenger, category: 'Accepted Ride',
|
||||||
|
);
|
||||||
} else {}
|
} else {}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,239 +1,218 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:slide_to_act/slide_to_act.dart';
|
import 'package:slide_to_act/slide_to_act.dart';
|
||||||
import 'package:vibration/vibration.dart';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import '../../../../constant/colors.dart';
|
import '../../../../constant/colors.dart';
|
||||||
import '../../../../constant/style.dart';
|
|
||||||
import '../../../../controller/home/captin/map_driver_controller.dart';
|
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||||
import '../../../widgets/elevated_btn.dart';
|
|
||||||
|
|
||||||
// Changed: إعادة تصميم كاملة للشريط ليصبح شريطًا علويًا عند بدء الرحلة
|
Widget driverEndRideBar() {
|
||||||
GetBuilder<MapDriverController> driverEndRideBar() {
|
|
||||||
return GetBuilder<MapDriverController>(
|
return GetBuilder<MapDriverController>(
|
||||||
builder: (controller) => AnimatedPositioned(
|
builder: (controller) {
|
||||||
duration: const Duration(milliseconds: 300),
|
// 🔥 فحص هل السعر ثابت للعرض
|
||||||
// New: يظهر الشريط من الأعلى عندما تبدأ الرحلة
|
final String carType = controller.carType;
|
||||||
top: controller.isRideStarted ? 0 : -200,
|
final bool isFixed = (carType == 'Speed' ||
|
||||||
left: 0,
|
carType == 'Awfar' ||
|
||||||
right: 0,
|
carType == 'Fixed Price');
|
||||||
child: Card(
|
|
||||||
margin: EdgeInsets.zero,
|
return AnimatedContainer(
|
||||||
elevation: 10,
|
duration: const Duration(milliseconds: 400),
|
||||||
shape: const RoundedRectangleBorder(
|
curve: Curves.easeOutBack,
|
||||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)),
|
transform: Matrix4.translationValues(
|
||||||
|
0,
|
||||||
|
controller.isRideStarted ? 0 : -300,
|
||||||
|
0,
|
||||||
|
), // Matrix4.translationValues مستخدمة للإزاحة [web:28]
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// -- معلومات الرحلة --
|
// لوحة العدادات
|
||||||
if (controller.carType != 'Mishwar Vip')
|
Container(
|
||||||
Row(
|
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 10),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF8F9FA),
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
_buildInfoColumn(
|
// 1) المسافة (تتغير دائماً)
|
||||||
icon: Icons.social_distance,
|
_buildLiveMetric(
|
||||||
text: '${controller.distance} ${'KM'.tr}',
|
icon: Icons.alt_route_rounded,
|
||||||
label: 'Distance'.tr,
|
iconColor: Colors.blueAccent,
|
||||||
|
value: controller.currentRideDistanceKm.toStringAsFixed(2),
|
||||||
|
unit: 'KM'.tr,
|
||||||
|
label: 'Traveled'.tr,
|
||||||
),
|
),
|
||||||
_buildInfoColumn(
|
|
||||||
icon: Icons.timelapse,
|
_buildVerticalDivider(),
|
||||||
text: controller.hours > 1
|
|
||||||
? '${controller.hours}h ${controller.minutes}m'
|
// 2) السعر (ثابت أو متغير)
|
||||||
: '${controller.minutes}m',
|
_buildLiveMetric(
|
||||||
label: 'Time'.tr,
|
icon: isFixed
|
||||||
|
? Icons.lock_outline_rounded
|
||||||
|
: Icons.attach_money_rounded,
|
||||||
|
iconColor: isFixed ? Colors.grey : AppColor.primaryColor,
|
||||||
|
value: NumberFormat('#,##0').format(
|
||||||
|
double.tryParse(controller.price.toString()) ?? 0,
|
||||||
),
|
),
|
||||||
_buildInfoColumn(
|
unit: 'SYP'.tr,
|
||||||
icon: Icons.money_sharp,
|
label: isFixed ? 'Fixed Price'.tr : 'Meter Fare'.tr,
|
||||||
text: '${controller.paymentAmount} ${'SYP'.tr}',
|
isHighlight: true,
|
||||||
label: 'Price'.tr,
|
isFixedStyle: isFixed,
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildVerticalDivider(),
|
||||||
|
|
||||||
|
// 3) الوقت (تصغير الخط + إخفاء الساعات إذا 0)
|
||||||
|
_buildLiveMetric(
|
||||||
|
icon: Icons.timer_outlined,
|
||||||
|
iconColor: Colors.orange,
|
||||||
|
value: _formatDurationFromStart(controller),
|
||||||
|
unit: '',
|
||||||
|
label: 'Duration'.tr,
|
||||||
|
valueFontSize: 14, // ✅ تصغير خط “المدة”
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (controller.carType != 'Mishwar Vip')
|
),
|
||||||
const Divider(height: 20),
|
|
||||||
|
|
||||||
// -- مؤقت الرحلة المتبقي (إن وجد) --
|
const SizedBox(height: 20),
|
||||||
_builtTimerAndCarType(),
|
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
// زر الإنهاء
|
||||||
|
|
||||||
// -- زر إنهاء الرحلة المنزلق --
|
|
||||||
SlideAction(
|
SlideAction(
|
||||||
height: 55,
|
key: ValueKey(controller.isRideFinished),
|
||||||
borderRadius: 15,
|
height: 60,
|
||||||
elevation: 4,
|
borderRadius: 18,
|
||||||
text: 'Slide to End Trip'.tr,
|
elevation: 0,
|
||||||
textStyle: AppStyle.title.copyWith(
|
text: 'Swipe to End Trip'.tr,
|
||||||
fontSize: 18,
|
textStyle: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
outerColor: AppColor.redColor,
|
outerColor: AppColor.redColor,
|
||||||
innerColor: Colors.white,
|
innerColor: Colors.white,
|
||||||
sliderButtonIcon: const Icon(
|
sliderButtonIcon: const Icon(
|
||||||
Icons.arrow_forward_ios,
|
Icons.stop_circle_outlined,
|
||||||
color: AppColor.redColor,
|
color: AppColor.redColor,
|
||||||
size: 24,
|
size: 32,
|
||||||
),
|
),
|
||||||
sliderRotate: false,
|
onSubmit: () async {
|
||||||
onSubmit: () {
|
await controller.finishRideFromDriver(isFromSlider: true);
|
||||||
HapticFeedback.mediumImpact();
|
return null;
|
||||||
controller.finishRideFromDriver();
|
|
||||||
return null; // New: onSubmit now returns null
|
|
||||||
},
|
},
|
||||||
),
|
), // SlideAction مثال الاستخدام موجود في صفحة الحزمة [web:19]
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
); // GetBuilder يعيد البناء عند update() من الكنترولر [web:21]
|
||||||
}
|
}
|
||||||
|
|
||||||
// New: ودجت لعرض معلومات الرحلة في الشريط العلوي
|
/// دالة تنسيق المدة:
|
||||||
Widget _buildInfoColumn(
|
/// - أقل من ساعة: mm:ss
|
||||||
{required IconData icon, required String text, required String label}) {
|
/// - ساعة فأكثر: h:mm:ss (خانة واحدة للساعات بدون leading zero)
|
||||||
return Column(
|
String _formatDurationFromStart(MapDriverController controller) {
|
||||||
mainAxisSize: MainAxisSize.min,
|
if (controller.rideStartTime == null) return "00:00";
|
||||||
children: [
|
|
||||||
Icon(icon, color: AppColor.primaryColor),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(text, style: AppStyle.title.copyWith(fontWeight: FontWeight.bold)),
|
|
||||||
Text(label,
|
|
||||||
style:
|
|
||||||
AppStyle.title.copyWith(color: Colors.grey[600], fontSize: 12)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Changed: تم تعديل تصميم ودجت عرض المؤقت ونوع السيارة
|
final d = DateTime.now().difference(controller.rideStartTime!);
|
||||||
class _builtTimerAndCarType extends StatelessWidget {
|
|
||||||
const _builtTimerAndCarType();
|
|
||||||
|
|
||||||
@override
|
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final controller = Get.find<MapDriverController>();
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// -- نوع السيارة --
|
|
||||||
Container(
|
|
||||||
decoration: AppStyle.boxDecoration1.copyWith(color: Colors.grey[200]),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
child: Text(
|
|
||||||
controller.carType,
|
|
||||||
style: AppStyle.title.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// -- مؤقت الرحلة --
|
|
||||||
if (controller.carType != 'Comfort' &&
|
|
||||||
controller.carType != 'Mishwar Vip' &&
|
|
||||||
controller.carType != 'Lady') ...[
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
height: 40,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
controller.remainingTimeTimerRideBegin < 60
|
|
||||||
? AppColor.redColor.withOpacity(0.8)
|
|
||||||
: AppColor.greenColor.withOpacity(0.8),
|
|
||||||
controller.remainingTimeTimerRideBegin < 60
|
|
||||||
? AppColor.redColor
|
|
||||||
: AppColor.greenColor,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
LinearProgressIndicator(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
|
||||||
Colors.white.withOpacity(0.2)),
|
|
||||||
minHeight: 40,
|
|
||||||
value: controller.progressTimerRideBegin.toDouble(),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
controller.stringRemainingTimeRideBegin,
|
|
||||||
style: AppStyle.title.copyWith(
|
|
||||||
color: Colors.white, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Changed: تم تعديل مكان ومظهر دائرة السرعة
|
final hours = d.inHours;
|
||||||
GetBuilder<MapDriverController> speedCircle() {
|
final minutes = d.inMinutes.remainder(60);
|
||||||
if (Get.find<MapDriverController>().speed > 100) {
|
final seconds = d.inSeconds.remainder(60);
|
||||||
if (Platform.isIOS) {
|
|
||||||
HapticFeedback.selectionClick();
|
if (hours == 0) {
|
||||||
} else {
|
// mm:ss
|
||||||
Vibration.vibrate(duration: 1000);
|
final totalMinutes = d.inMinutes;
|
||||||
|
return "${twoDigits(totalMinutes)}:${twoDigits(seconds)}";
|
||||||
}
|
}
|
||||||
Get.defaultDialog(
|
|
||||||
barrierDismissible: false,
|
// h:mm:ss
|
||||||
titleStyle: AppStyle.title,
|
return "$hours:${twoDigits(minutes)}:${twoDigits(seconds)}";
|
||||||
title: 'Speed Over'.tr,
|
} // Duration وتفكيكه (inHours/inMinutes/inSeconds) من أساسيات Dart [web:11]
|
||||||
middleText: 'Please slow down'.tr,
|
|
||||||
middleTextStyle: AppStyle.title,
|
Widget _buildLiveMetric({
|
||||||
confirm: MyElevatedButton(
|
required IconData icon,
|
||||||
title: 'I will slow down'.tr,
|
required Color iconColor,
|
||||||
onPressed: () => Get.back(),
|
required String value,
|
||||||
),
|
required String unit,
|
||||||
);
|
required String label,
|
||||||
}
|
bool isHighlight = false,
|
||||||
return GetBuilder<MapDriverController>(
|
bool isFixedStyle = false,
|
||||||
builder: (controller) {
|
double? valueFontSize, // ✅ جديد: حجم خط القيمة فقط
|
||||||
return controller.isRideStarted
|
}) {
|
||||||
? Positioned(
|
final effectiveFontSize = valueFontSize ?? (isHighlight ? 20 : 18);
|
||||||
// New: تم وضع دائرة السرعة في الأسفل يمينًا
|
|
||||||
bottom: 25,
|
return Expanded(
|
||||||
left: 3,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: Colors.white,
|
|
||||||
boxShadow: [BoxShadow(blurRadius: 5, color: Colors.black26)],
|
|
||||||
border: Border.all(
|
|
||||||
width: 4,
|
|
||||||
color: controller.speed > 100
|
|
||||||
? Colors.red
|
|
||||||
: AppColor.greenColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
height: 70,
|
|
||||||
width: 70,
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
Icon(icon, size: 14, color: iconColor),
|
||||||
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
controller.speed.toStringAsFixed(0),
|
label,
|
||||||
style: AppStyle.number.copyWith(fontSize: 24),
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text("km/h", style: TextStyle(fontSize: 10)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
|
textBaseline: TextBaseline.alphabetic,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: effectiveFontSize, // ✅ هنا صار التحكم
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: isFixedStyle
|
||||||
|
? Colors.grey[800]
|
||||||
|
: (isHighlight ? AppColor.primaryColor : Colors.black87),
|
||||||
|
fontFamily: 'monospace',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
if (unit.isNotEmpty) ...[
|
||||||
: const SizedBox();
|
const SizedBox(width: 2),
|
||||||
},
|
Text(
|
||||||
|
unit,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildVerticalDivider() {
|
||||||
|
return Container(height: 35, width: 1, color: Colors.grey.shade300);
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,16 +55,16 @@ class GoogleDriverMap extends StatelessWidget {
|
|||||||
trafficEnabled: true, // Changed: تفعيل عرض حركة المرور
|
trafficEnabled: true, // Changed: تفعيل عرض حركة المرور
|
||||||
buildingsEnabled: true,
|
buildingsEnabled: true,
|
||||||
polylines: {
|
polylines: {
|
||||||
Polyline(
|
// Polyline(
|
||||||
zIndex: 2,
|
// zIndex: 2,
|
||||||
|
|
||||||
polylineId: const PolylineId('route1'),
|
// polylineId: const PolylineId('route1'),
|
||||||
points: controller.polylineCoordinates,
|
// points: controller.polylineCoordinates,
|
||||||
color: const Color.fromARGB(255, 163, 81, 246),
|
// color: const Color.fromARGB(255, 163, 81, 246),
|
||||||
width: 6, // Changed: زيادة عرض الخط
|
// width: 6, // Changed: زيادة عرض الخط
|
||||||
startCap: Cap.roundCap,
|
// startCap: Cap.roundCap,
|
||||||
endCap: Cap.roundCap,
|
// endCap: Cap.roundCap,
|
||||||
),
|
// ),
|
||||||
// Polyline(
|
// Polyline(
|
||||||
// zIndex: 2,
|
// zIndex: 2,
|
||||||
|
|
||||||
|
|||||||
@@ -12,43 +12,51 @@ class GoogleMapApp extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapDriverController>(
|
return GetBuilder<MapDriverController>(
|
||||||
builder: (mapDriverController) => mapDriverController.isRideStarted
|
builder: (mapDriverController) {
|
||||||
? Positioned(
|
if (!mapDriverController.isRideStarted) return const SizedBox();
|
||||||
right: 3,
|
|
||||||
bottom: 20,
|
|
||||||
child: Container(
|
|
||||||
decoration: AppStyle.boxDecoration,
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
var startLat = Get.find<MapDriverController>()
|
|
||||||
.latLngPassengerLocation
|
|
||||||
.latitude;
|
|
||||||
var startLng = Get.find<MapDriverController>()
|
|
||||||
.latLngPassengerLocation
|
|
||||||
.longitude;
|
|
||||||
|
|
||||||
var endLat = Get.find<MapDriverController>()
|
// REMOVED: Positioned wrapper
|
||||||
.latLngPassengerDestination
|
return Material(
|
||||||
.latitude;
|
elevation: 8,
|
||||||
var endLng = Get.find<MapDriverController>()
|
shadowColor: Colors.black26,
|
||||||
.latLngPassengerDestination
|
borderRadius: BorderRadius.circular(30),
|
||||||
.longitude;
|
color: Colors.white,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
onTap: () async {
|
||||||
|
var endLat =
|
||||||
|
mapDriverController.latLngPassengerDestination.latitude;
|
||||||
|
var endLng =
|
||||||
|
mapDriverController.latLngPassengerDestination.longitude;
|
||||||
|
String url = 'google.navigation:q=$endLat,$endLng';
|
||||||
|
|
||||||
String url =
|
|
||||||
'https://www.google.com/maps/dir/$startLat,$startLng/$endLat,$endLng/&directionsmode=driving';
|
|
||||||
if (await canLaunchUrl(Uri.parse(url))) {
|
if (await canLaunchUrl(Uri.parse(url))) {
|
||||||
await launchUrl(Uri.parse(url));
|
await launchUrl(Uri.parse(url));
|
||||||
} else {
|
} else {
|
||||||
throw 'Could not launch google maps';
|
String webUrl =
|
||||||
|
'https://www.google.com/maps/dir/?api=1&destination=$endLat,$endLng';
|
||||||
|
if (await canLaunchUrl(Uri.parse(webUrl))) {
|
||||||
|
await launchUrl(Uri.parse(webUrl));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
child: Container(
|
||||||
MaterialCommunityIcons.map_marker_radius,
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
size: 45,
|
decoration: BoxDecoration(
|
||||||
color: AppColor.blueColor,
|
color: Colors.blueAccent,
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColor.blueColor.withOpacity(0.2), width: 1),
|
||||||
),
|
),
|
||||||
)),
|
child: const Icon(
|
||||||
)
|
MaterialCommunityIcons.google_maps,
|
||||||
: const SizedBox());
|
size: 28,
|
||||||
|
color: AppColor.secondaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,256 +1,392 @@
|
|||||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sefer_driver/constant/colors.dart';
|
import 'package:sefer_driver/constant/colors.dart';
|
||||||
import 'package:sefer_driver/constant/info.dart';
|
|
||||||
import 'package:sefer_driver/controller/firebase/firbase_messge.dart';
|
|
||||||
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
|
import 'package:sefer_driver/controller/home/captin/map_driver_controller.dart';
|
||||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
|
||||||
|
|
||||||
import '../../../../constant/box_name.dart';
|
|
||||||
import '../../../../constant/style.dart';
|
import '../../../../constant/style.dart';
|
||||||
import '../../../../main.dart';
|
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||||
|
import '../../../../controller/functions/launch.dart';
|
||||||
|
import '../../../../controller/functions/location_controller.dart';
|
||||||
|
import '../../../widgets/error_snakbar.dart';
|
||||||
|
|
||||||
// Changed: إعادة تصميم كاملة لتصبح شريط معلومات علوي مدمج
|
|
||||||
class PassengerInfoWindow extends StatelessWidget {
|
class PassengerInfoWindow extends StatelessWidget {
|
||||||
const PassengerInfoWindow({super.key});
|
const PassengerInfoWindow({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// 1. حساب الهوامش الآمنة (SafeArea) من الأسفل
|
||||||
|
final double safeBottomPadding = MediaQuery.of(context).padding.bottom;
|
||||||
|
|
||||||
return GetBuilder<MapDriverController>(
|
return GetBuilder<MapDriverController>(
|
||||||
builder: (controller) => AnimatedPositioned(
|
// id: 'PassengerInfo',
|
||||||
duration: const Duration(milliseconds: 400),
|
builder: (controller) {
|
||||||
curve: Curves.easeInOut,
|
// --- 1. تجهيز بيانات العرض ---
|
||||||
// Changed: تم تغيير الموضع من الأسفل إلى الأعلى
|
String displayName = controller.passengerName ?? "Unknown";
|
||||||
top: controller.isPassengerInfoWindow ? 15.0 : -200.0,
|
String avatarText = "";
|
||||||
left: 15.0,
|
|
||||||
right: 15.0,
|
// التحقق من اللغة (عربي/إنجليزي) للاسم المختصر
|
||||||
child: Card(
|
bool isArabic = RegExp(r'[\u0600-\u06FF]').hasMatch(displayName);
|
||||||
elevation: 8,
|
|
||||||
shadowColor: Colors.black.withOpacity(0.3),
|
if (displayName.isNotEmpty) {
|
||||||
shape: RoundedRectangleBorder(
|
if (isArabic) {
|
||||||
borderRadius: BorderRadius.circular(16),
|
avatarText = displayName.split(' ').first;
|
||||||
|
if (avatarText.length > 4) {
|
||||||
|
avatarText = avatarText.substring(0, 4);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
avatarText = displayName[0].toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. المنطق الذكي للموقع (Smart Positioning) ---
|
||||||
|
// نرفع النافذة إذا ظهر شريط التعليمات في الأسفل لتجنب التغطية
|
||||||
|
bool hasInstructions = controller.currentInstruction.isNotEmpty;
|
||||||
|
double instructionsHeight = hasInstructions ? 110.0 : 0.0;
|
||||||
|
|
||||||
|
// الموقع النهائي: إذا كانت مفعلة تظهر، وإلا تختفي للأسفل
|
||||||
|
double finalBottomPosition = controller.isPassengerInfoWindow
|
||||||
|
? (safeBottomPadding + 10 + instructionsHeight)
|
||||||
|
: -450.0;
|
||||||
|
|
||||||
|
return AnimatedPositioned(
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
curve: Curves.fastOutSlowIn,
|
||||||
|
bottom: finalBottomPosition,
|
||||||
|
left: 12.0,
|
||||||
|
right: 12.0,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.15),
|
||||||
|
blurRadius: 25,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
spreadRadius: 2,
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// New: صف علوي للمعلومات الأساسية
|
// --- مقبض السحب (Visual Handle) ---
|
||||||
_buildTopInfoRow(controller),
|
Center(
|
||||||
const Divider(height: 16),
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(top: 8, bottom: 4),
|
||||||
// Changed: الأزرار الآن في صف أفقي ومدمج
|
width: 40,
|
||||||
if (!controller.isRideBegin) _buildActionButtons(controller),
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
// New: مؤشر انتظار الراكب المدمج
|
color: Colors.grey.shade300,
|
||||||
if (controller.remainingTimeInPassengerLocatioWait < 300 &&
|
borderRadius: BorderRadius.circular(10),
|
||||||
controller.remainingTimeInPassengerLocatioWait != 0 &&
|
|
||||||
!controller.isRideBegin) ...[
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
_buildWaitingIndicator(controller),
|
|
||||||
],
|
|
||||||
|
|
||||||
// زر الإلغاء بعد انتهاء وقت الانتظار
|
|
||||||
if (controller.isdriverWaitTimeEnd &&
|
|
||||||
!controller.isRideBegin) ...[
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
_buildCancelAfterWaitButton(controller),
|
|
||||||
]
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// New: ودجت لعرض المعلومات العلوية بشكل مدمج
|
Padding(
|
||||||
Widget _buildTopInfoRow(MapDriverController controller) {
|
padding: const EdgeInsets.fromLTRB(16, 4, 16, 16),
|
||||||
return Row(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
// معلومات الراكب
|
// --- الصف العلوي: معلومات الراكب ---
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// الصورة الرمزية
|
||||||
|
Container(
|
||||||
|
width: 52,
|
||||||
|
height: 52,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: AppColor.primaryColor.withOpacity(0.1),
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
AppColor.primaryColor.withOpacity(0.2)),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
avatarText,
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColor.primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: isArabic ? 14 : 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// النصوص (الاسم والمسافة)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Go to passenger:'.tr,
|
displayName,
|
||||||
style: AppStyle.title
|
style: AppStyle.title.copyWith(
|
||||||
.copyWith(color: Colors.grey[600], fontSize: 13),
|
fontWeight: FontWeight.w800,
|
||||||
),
|
fontSize: 16),
|
||||||
Text(
|
maxLines: 1,
|
||||||
controller.passengerName,
|
|
||||||
style: AppStyle.title
|
|
||||||
.copyWith(fontWeight: FontWeight.bold, fontSize: 18),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(height: 4),
|
||||||
),
|
|
||||||
),
|
|
||||||
// معلومات المسافة والزمن
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_buildInfoChip(Icons.map_outlined, '${controller.distance} km'),
|
Icon(Icons.location_on,
|
||||||
const SizedBox(width: 8),
|
size: 14, color: Colors.grey[600]),
|
||||||
_buildInfoChip(
|
|
||||||
Icons.timer_outlined,
|
|
||||||
controller.hours > 1
|
|
||||||
? '${controller.hours}h ${controller.minutes}m'
|
|
||||||
: '${controller.minutes}m',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// New: ودجت مخصص لعرض المعلومات بشكل أنيق
|
|
||||||
Widget _buildInfoChip(IconData icon, String text) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColor.primaryColor.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: AppColor.primaryColor, size: 16),
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(text,
|
Text(
|
||||||
|
'${controller.distance} km',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColor.primaryColor, fontWeight: FontWeight.bold)),
|
color: Colors.grey[700],
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Icon(Icons.access_time_filled,
|
||||||
|
size: 14, color: Colors.grey[600]),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
controller.hours > 0
|
||||||
|
? '${controller.hours}h ${controller.minutes}m'
|
||||||
|
: '${controller.minutes} min',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
}
|
|
||||||
|
|
||||||
// Changed: إعادة تصميم أزرار الإجراءات لتكون أكثر دمجًا
|
|
||||||
Widget _buildActionButtons(MapDriverController controller) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
if (controller.isArrivedSend)
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
icon: const Icon(Icons.location_on, size: 18),
|
|
||||||
label: Text('I Arrive'.tr),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppColor.yellowColor,
|
|
||||||
foregroundColor: Colors.black,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(10)),
|
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
),
|
||||||
controller.getRoute(
|
|
||||||
origin: controller.latLngPassengerLocation,
|
// أزرار جانبية (سرعة + اتصال)
|
||||||
destination: controller.latLngPassengerDestination,
|
Row(
|
||||||
routeColor: Colors.blue // أو أي لون
|
children: [
|
||||||
);
|
_buildSpeedCircle(),
|
||||||
if (await controller
|
const SizedBox(width: 10),
|
||||||
.calculateDistanceBetweenDriverAndPassengerLocation() <
|
InkWell(
|
||||||
140) {
|
onTap: () async {
|
||||||
Get.find<FirebaseMessagesController>()
|
controller.isSocialPressed = true;
|
||||||
.sendNotificationToDriverMAP(
|
|
||||||
'Hi ,I Arrive your site',
|
// نفحص النتيجة: هل مسموح له يتصل؟
|
||||||
'I Arrive at your site'.tr,
|
bool canCall =
|
||||||
controller.tokenPassenger,
|
await controller.driverCallPassenger();
|
||||||
[],
|
|
||||||
'ding.wav',
|
if (canCall) {
|
||||||
);
|
makePhoneCall(
|
||||||
controller.startTimerToShowDriverWaitPassengerDuration();
|
controller.passengerPhone.toString());
|
||||||
controller.isArrivedSend = false;
|
|
||||||
} else {
|
} else {
|
||||||
MyDialog().getDialog(
|
// هنا ممكن تظهر رسالة: تم منع الاتصال بسبب كثرة الإلغاءات
|
||||||
'You are not near the passenger location'.tr,
|
mySnackeBarError(
|
||||||
'Please go to the pickup location exactly'.tr,
|
"You cannot call the passenger due to policy violations"
|
||||||
() => Get.back());
|
.tr);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.green.withOpacity(0.2)),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.phone,
|
||||||
|
color: Colors.green, size: 22),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (controller.isArrivedSend) const SizedBox(width: 8),
|
],
|
||||||
Expanded(
|
),
|
||||||
flex: 2,
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// خط فاصل
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: Divider(height: 1, color: Colors.grey.shade100),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- مؤشر الانتظار (يظهر عند الوصول) ---
|
||||||
|
if (controller.remainingTimeInPassengerLocatioWait <
|
||||||
|
300 &&
|
||||||
|
controller.remainingTimeInPassengerLocatioWait != 0 &&
|
||||||
|
!controller.isRideBegin) ...[
|
||||||
|
_buildWaitingIndicator(controller),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
|
||||||
|
// --- الأزرار الرئيسية (وصلت / ابدأ) ---
|
||||||
|
if (!controller.isRideBegin)
|
||||||
|
_buildActionButtons(controller),
|
||||||
|
|
||||||
|
// --- زر الإلغاء المدفوع (بعد انتهاء وقت الانتظار) ---
|
||||||
|
if (controller.isdriverWaitTimeEnd &&
|
||||||
|
!controller.isRideBegin)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 48,
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.play_arrow_rounded, size: 20),
|
|
||||||
label: Text('Start the Ride'.tr,
|
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColor.greenColor,
|
backgroundColor: const Color(0xFFFFF0F0),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.red,
|
||||||
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10)),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: const BorderSide(
|
||||||
|
color: Color(0xFFFFCDCD)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
MyDialog().getDialog(
|
MyDialog().getDialog(
|
||||||
"Is the Passenger in your Car?".tr,
|
'Confirm Cancellation'.tr,
|
||||||
"Don't start trip if passenger not in your car".tr,
|
'Are you sure you want to cancel and collect the fee?'
|
||||||
|
.tr, () async {
|
||||||
|
// كود الإلغاء
|
||||||
|
Get.back();
|
||||||
|
controller
|
||||||
|
.addWaitingTimeCostFromPassengerToDriverWallet();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.money_off, size: 20),
|
||||||
|
label: Text('Cancel & Collect Fee'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Widgets مساعدة ---
|
||||||
|
|
||||||
|
Widget _buildSpeedCircle() {
|
||||||
|
return GetBuilder<LocationController>(builder: (locController) {
|
||||||
|
int speedKmh = (locController.speed * 3.6).round();
|
||||||
|
Color color = speedKmh > 100 ? Colors.red : const Color(0xFF0D47A1);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: 42,
|
||||||
|
height: 42,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: color.withOpacity(0.3), width: 2),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text('$speedKmh',
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontSize: 13,
|
||||||
|
height: 1)),
|
||||||
|
Text('km/h',
|
||||||
|
style: TextStyle(
|
||||||
|
color: color.withOpacity(0.7), fontSize: 8, height: 1)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildWaitingIndicator(MapDriverController controller) {
|
||||||
|
bool isUrgent = controller.remainingTimeInPassengerLocatioWait < 60;
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.timer_outlined,
|
||||||
|
size: 16, color: isUrgent ? Colors.red : Colors.green),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value:
|
||||||
|
controller.progressInPassengerLocationFromDriver.toDouble(),
|
||||||
|
backgroundColor: Colors.grey[200],
|
||||||
|
color: isUrgent ? Colors.red : Colors.green,
|
||||||
|
minHeight: 6,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
controller.stringRemainingTimeWaitingPassenger,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: isUrgent ? Colors.red : Colors.green,
|
||||||
|
fontFamily: 'monospace'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionButtons(MapDriverController controller) {
|
||||||
|
if (controller.isArrivedSend) {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFFF1C40F),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 2,
|
||||||
|
shape:
|
||||||
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
await controller.markDriverAsArrived();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.near_me_rounded),
|
||||||
|
label: Text('I Have Arrived'.tr,
|
||||||
|
style:
|
||||||
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF27AE60),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 2,
|
||||||
|
shape:
|
||||||
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
MyDialog().getDialog(
|
||||||
|
"Start Trip?".tr,
|
||||||
|
"Ensure the passenger is in the car.".tr,
|
||||||
() async {
|
() async {
|
||||||
await controller.startRideFromDriver();
|
await controller.startRideFromDriver();
|
||||||
Get.back();
|
Get.back();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
icon: const Icon(Icons.play_circle_fill_rounded),
|
||||||
),
|
label: Text('Start Ride'.tr,
|
||||||
],
|
style:
|
||||||
);
|
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||||
}
|
|
||||||
|
|
||||||
// Changed: مؤشر الانتظار الآن أكثر دمجًا
|
|
||||||
Widget _buildWaitingIndicator(MapDriverController controller) {
|
|
||||||
return ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
LinearProgressIndicator(
|
|
||||||
backgroundColor: AppColor.greyColor.withOpacity(0.3),
|
|
||||||
color: controller.remainingTimeInPassengerLocatioWait < 60
|
|
||||||
? AppColor.redColor
|
|
||||||
: AppColor.greenColor,
|
|
||||||
minHeight: 25,
|
|
||||||
value: controller.progressInPassengerLocationFromDriver.toDouble(),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
controller.stringRemainingTimeWaitingPassenger,
|
|
||||||
style: AppStyle.title.copyWith(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 13,
|
|
||||||
shadows: [
|
|
||||||
Shadow(color: Colors.black.withOpacity(0.5), blurRadius: 2)
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// New: زر الإلغاء بعد انتهاء الانتظار
|
|
||||||
Widget _buildCancelAfterWaitButton(MapDriverController controller) {
|
|
||||||
return MyElevatedButton(
|
|
||||||
title: 'You Can Cancel the Trip and get Cost From '.tr +
|
|
||||||
AppInformation.appName.tr,
|
|
||||||
kolor: AppColor.deepPurpleAccent,
|
|
||||||
onPressed: () {
|
|
||||||
MyDialog().getDialog('Are you sure to cancel?'.tr, '', () async {
|
|
||||||
Get.find<FirebaseMessagesController>().sendNotificationToDriverMAP(
|
|
||||||
'Driver Cancelled Your Trip',
|
|
||||||
'You will need to pay the cost to the driver, or it will be deducted from your next trip'
|
|
||||||
.tr,
|
|
||||||
controller.tokenPassenger,
|
|
||||||
[],
|
|
||||||
'cancel.wav');
|
|
||||||
box.write(BoxName.rideStatus, 'Cancel');
|
|
||||||
await controller.addWaitingTimeCostFromPassengerToDriverWallet();
|
|
||||||
controller.isdriverWaitTimeEnd = false;
|
|
||||||
Get.back();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,258 +1,89 @@
|
|||||||
// import 'dart:io';
|
|
||||||
|
|
||||||
// import 'package:bubble_head/bubble.dart';
|
|
||||||
// import 'package:flutter/material.dart';
|
|
||||||
// import 'package:flutter_font_icons/flutter_font_icons.dart';
|
|
||||||
// import 'package:get/get.dart';
|
|
||||||
// import 'package:sefer_driver/constant/info.dart';
|
|
||||||
// import 'package:sefer_driver/controller/functions/location_controller.dart';
|
|
||||||
// import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
|
||||||
// import 'package:sefer_driver/views/widgets/my_textField.dart';
|
|
||||||
// import 'package:url_launcher/url_launcher.dart';
|
|
||||||
|
|
||||||
// import '../../../../constant/box_name.dart';
|
|
||||||
// import '../../../../constant/colors.dart';
|
|
||||||
// import '../../../../constant/style.dart';
|
|
||||||
// import '../../../../controller/functions/launch.dart';
|
|
||||||
// import '../../../../controller/home/captin/map_driver_controller.dart';
|
|
||||||
// import '../../../../main.dart';
|
|
||||||
|
|
||||||
// class SosConnect extends StatelessWidget {
|
|
||||||
// const SosConnect({super.key});
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// Widget build(BuildContext context) {
|
|
||||||
// return GetBuilder<MapDriverController>(
|
|
||||||
// builder: (mapDriverController) => mapDriverController.isRideStarted
|
|
||||||
// ? Positioned(
|
|
||||||
// left: 16,
|
|
||||||
// bottom: 16,
|
|
||||||
// child: Card(
|
|
||||||
// elevation: 4,
|
|
||||||
// shape: RoundedRectangleBorder(
|
|
||||||
// borderRadius: BorderRadius.circular(12),
|
|
||||||
// ),
|
|
||||||
// child: SizedBox(
|
|
||||||
// height: 60,
|
|
||||||
// width: 180,
|
|
||||||
// child: Row(
|
|
||||||
// mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
// children: [
|
|
||||||
// IconButton(
|
|
||||||
// onPressed: () {
|
|
||||||
// _handleSosCall(mapDriverController);
|
|
||||||
// },
|
|
||||||
// icon: const Icon(
|
|
||||||
// Icons.sos_sharp,
|
|
||||||
// size: 32,
|
|
||||||
// color: AppColor.redColor,
|
|
||||||
// ),
|
|
||||||
// tooltip: 'SOS - Call Emergency',
|
|
||||||
// ),
|
|
||||||
// VerticalDivider(
|
|
||||||
// color: Colors.grey[300],
|
|
||||||
// thickness: 1,
|
|
||||||
// ),
|
|
||||||
// IconButton(
|
|
||||||
// onPressed: () {
|
|
||||||
// _handleWhatsApp(mapDriverController);
|
|
||||||
// },
|
|
||||||
// icon: const Icon(
|
|
||||||
// FontAwesome.whatsapp,
|
|
||||||
// color: AppColor.greenColor,
|
|
||||||
// size: 32,
|
|
||||||
// ),
|
|
||||||
// tooltip: 'SOS - Send WhatsApp Message',
|
|
||||||
// ),
|
|
||||||
// VerticalDivider(
|
|
||||||
// color: Colors.grey[300],
|
|
||||||
// thickness: 1,
|
|
||||||
// ),
|
|
||||||
// IconButton(
|
|
||||||
// onPressed: () {
|
|
||||||
// _handleGoogleMap(mapDriverController);
|
|
||||||
// },
|
|
||||||
// icon: const Icon(
|
|
||||||
// MaterialCommunityIcons.map_marker_radius,
|
|
||||||
// color: AppColor.primaryColor,
|
|
||||||
// size: 32,
|
|
||||||
// ),
|
|
||||||
// tooltip: 'Google Maps - Navigate',
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// )
|
|
||||||
// : const SizedBox(),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// void _handleSosCall(MapDriverController mapDriverController) {
|
|
||||||
// if (box.read(BoxName.sosPhoneDriver) == null) {
|
|
||||||
// Get.defaultDialog(
|
|
||||||
// title: 'Insert Emergency Number'.tr,
|
|
||||||
// content: Form(
|
|
||||||
// key: mapDriverController.formKey1,
|
|
||||||
// child: MyTextForm(
|
|
||||||
// controller: mapDriverController.sosEmergincyNumberCotroller,
|
|
||||||
// label: 'Emergency Number'.tr,
|
|
||||||
// hint: 'Enter phone number'.tr,
|
|
||||||
// type: TextInputType.phone,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// confirm: MyElevatedButton(
|
|
||||||
// title: 'Save'.tr,
|
|
||||||
// onPressed: () {
|
|
||||||
// if (mapDriverController.formKey1.currentState!.validate()) {
|
|
||||||
// box.write(BoxName.sosPhoneDriver,
|
|
||||||
// mapDriverController.sosEmergincyNumberCotroller.text);
|
|
||||||
// Get.back(); // Close the dialog
|
|
||||||
// launchCommunication(
|
|
||||||
// 'phone', box.read(BoxName.sosPhoneDriver), '');
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// } else {
|
|
||||||
// launchCommunication('phone', box.read(BoxName.sosPhoneDriver), '');
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// void _handleWhatsApp(MapDriverController mapDriverController) {
|
|
||||||
// if (box.read(BoxName.sosPhoneDriver) == null) {
|
|
||||||
// Get.defaultDialog(
|
|
||||||
// title: 'Insert Emergency Number'.tr,
|
|
||||||
// content: Form(
|
|
||||||
// key: mapDriverController.formKey1,
|
|
||||||
// child: MyTextForm(
|
|
||||||
// controller: mapDriverController.sosEmergincyNumberCotroller,
|
|
||||||
// label: 'Emergency Number'.tr,
|
|
||||||
// hint: 'Enter phone number'.tr,
|
|
||||||
// type: TextInputType.phone,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// confirm: MyElevatedButton(
|
|
||||||
// title: 'Save'.tr,
|
|
||||||
// onPressed: () {
|
|
||||||
// if (mapDriverController.formKey1.currentState!.validate()) {
|
|
||||||
// box.write(BoxName.sosPhoneDriver,
|
|
||||||
// mapDriverController.sosEmergincyNumberCotroller.text);
|
|
||||||
// Get.back(); // Close the dialog
|
|
||||||
// _sendWhatsAppMessage(mapDriverController);
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// } else {
|
|
||||||
// _sendWhatsAppMessage(mapDriverController);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// void _handleGoogleMap(MapDriverController mapDriverController) {
|
|
||||||
// () async {
|
|
||||||
// if (Platform.isAndroid) {
|
|
||||||
// Bubble().startBubbleHead(sendAppToBackground: true);
|
|
||||||
// }
|
|
||||||
// var startLat =
|
|
||||||
// Get.find<MapDriverController>().latLngPassengerLocation.latitude;
|
|
||||||
// var startLng =
|
|
||||||
// Get.find<MapDriverController>().latLngPassengerLocation.longitude;
|
|
||||||
|
|
||||||
// var endLat =
|
|
||||||
// Get.find<MapDriverController>().latLngPassengerDestination.latitude;
|
|
||||||
// var endLng =
|
|
||||||
// Get.find<MapDriverController>().latLngPassengerDestination.longitude;
|
|
||||||
|
|
||||||
// String url =
|
|
||||||
// 'https://www.google.com/maps/dir/$startLat,$startLng/$endLat,$endLng/&directionsmode=driving';
|
|
||||||
// if (await canLaunchUrl(Uri.parse(url))) {
|
|
||||||
// await launchUrl(Uri.parse(url));
|
|
||||||
// } else {
|
|
||||||
// throw 'Could not launch google maps';
|
|
||||||
// }
|
|
||||||
// }();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// void _sendWhatsAppMessage(MapDriverController mapDriverController) {
|
|
||||||
// final sosNumber = box.read(BoxName.sosPhoneDriver);
|
|
||||||
// if (sosNumber != null) {
|
|
||||||
// launchCommunication(
|
|
||||||
// 'whatsapp',
|
|
||||||
// '+2$sosNumber', // Consider international format
|
|
||||||
// "${"Hello, this is Driver".tr} ${box.read(BoxName.nameDriver)}. "
|
|
||||||
// "${"My current location is:".tr} "
|
|
||||||
// "https://www.google.com/maps/place/"
|
|
||||||
// "${Get.find<LocationController>().myLocation.latitude},"
|
|
||||||
// "${Get.find<LocationController>().myLocation.longitude} "
|
|
||||||
// "${"\nI have a trip on".tr} ${AppInformation.appName} "
|
|
||||||
// "${"app with passenger".tr} ${mapDriverController.passengerName}.",
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sefer_driver/views/widgets/elevated_btn.dart';
|
import 'package:sefer_driver/views/widgets/error_snakbar.dart';
|
||||||
import 'package:sefer_driver/views/widgets/my_textField.dart';
|
import 'package:sefer_driver/views/widgets/my_textField.dart';
|
||||||
|
import 'package:sefer_driver/views/widgets/elevated_btn.dart'; // Checked import
|
||||||
|
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||||
|
|
||||||
import '../../../../constant/box_name.dart';
|
import '../../../../constant/box_name.dart';
|
||||||
import '../../../../constant/colors.dart';
|
import '../../../../constant/colors.dart';
|
||||||
import '../../../../constant/style.dart';
|
import '../../../../constant/style.dart';
|
||||||
import '../../../../controller/firebase/firbase_messge.dart';
|
import '../../../../controller/firebase/notification_service.dart';
|
||||||
import '../../../../controller/functions/launch.dart';
|
import '../../../../controller/functions/launch.dart';
|
||||||
import '../../../../controller/home/captin/map_driver_controller.dart';
|
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||||
import '../../../../main.dart';
|
import '../../../../main.dart';
|
||||||
|
|
||||||
// Changed: إعادة تصميم وتغيير موضع أزرار التواصل والطوارئ
|
|
||||||
class SosConnect extends StatelessWidget {
|
class SosConnect extends StatelessWidget {
|
||||||
const SosConnect({super.key});
|
SosConnect({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GetBuilder<MapDriverController>(
|
return GetBuilder<MapDriverController>(
|
||||||
|
id: 'SosConnect', // Keep ID for updates
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
// New: تجميع الأزرار في عمود واحد على الجانب الأيمن
|
// Check visibility logic
|
||||||
return Positioned(
|
bool showPassengerContact =
|
||||||
bottom: 110, // New: فوق عداد السرعة
|
!controller.isRideBegin && controller.isPassengerInfoWindow;
|
||||||
right: 16,
|
bool showSos = controller.isRideStarted;
|
||||||
|
|
||||||
|
if (!showPassengerContact && !showSos) return const SizedBox();
|
||||||
|
|
||||||
|
// REMOVED: Positioned widget
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.95),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// زر الاتصال بالراكب (يظهر قبل بدء الرحلة)
|
// === Call Button ===
|
||||||
if (!controller.isRideBegin && controller.isPassengerInfoWindow)
|
if (showPassengerContact)
|
||||||
_buildSocialButton(
|
_buildModernActionButton(
|
||||||
icon: Icons.phone,
|
icon: Icons.phone_in_talk,
|
||||||
color: AppColor.blueColor,
|
color: Colors.white,
|
||||||
|
bgColor: AppColor.blueColor,
|
||||||
tooltip: 'Call Passenger',
|
tooltip: 'Call Passenger',
|
||||||
onPressed: () async {
|
onTap: () async {
|
||||||
controller.isSocialPressed = true;
|
controller.isSocialPressed = true;
|
||||||
await controller.driverCallPassenger();
|
bool canCall = await controller.driverCallPassenger();
|
||||||
|
if (canCall) {
|
||||||
makePhoneCall(controller.passengerPhone.toString());
|
makePhoneCall(controller.passengerPhone.toString());
|
||||||
|
} else {
|
||||||
|
mySnackeBarError("Policy restriction on calls".tr);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
// زر الرسائل للراكب (يظهر قبل بدء الرحلة)
|
if (showPassengerContact) const SizedBox(height: 12),
|
||||||
if (!controller.isRideBegin && controller.isPassengerInfoWindow)
|
|
||||||
const SizedBox(height: 12),
|
// === Message Button ===
|
||||||
if (!controller.isRideBegin && controller.isPassengerInfoWindow)
|
if (showPassengerContact)
|
||||||
_buildSocialButton(
|
_buildModernActionButton(
|
||||||
icon: Icons.message,
|
icon: MaterialCommunityIcons.message_text_outline,
|
||||||
color: AppColor.greenColor,
|
color: AppColor.primaryColor,
|
||||||
tooltip: 'Send Message',
|
bgColor: Colors.grey.shade100,
|
||||||
onPressed: () {
|
tooltip: 'Message Passenger',
|
||||||
// الكود الخاص بنافذة الرسائل السريعة
|
onTap: () => _showMessageOptions(context, controller),
|
||||||
_showMessageOptions(context, controller);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// زر الطوارئ (SOS) (يظهر بعد بدء الرحلة)
|
// === SOS Button ===
|
||||||
if (controller.isRideStarted)
|
if (showSos)
|
||||||
_buildSocialButton(
|
_buildModernActionButton(
|
||||||
icon: Icons.sos_sharp,
|
icon: MaterialIcons.warning,
|
||||||
color: AppColor.redColor,
|
color: Colors.white,
|
||||||
tooltip: 'SOS - Call Emergency',
|
bgColor: AppColor.redColor,
|
||||||
onPressed: () => _handleSosCall(controller),
|
tooltip: 'EMERGENCY SOS',
|
||||||
|
isPulsing: true,
|
||||||
|
onTap: () => _handleSosCall(controller),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -261,42 +92,62 @@ class SosConnect extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// New: ودجت منفصل لبناء أزرار التواصل
|
Widget _buildModernActionButton({
|
||||||
Widget _buildSocialButton(
|
required IconData icon,
|
||||||
{required IconData icon,
|
|
||||||
required Color color,
|
required Color color,
|
||||||
|
required Color bgColor,
|
||||||
required String tooltip,
|
required String tooltip,
|
||||||
required VoidCallback onPressed}) {
|
required VoidCallback onTap,
|
||||||
return Container(
|
bool isPulsing = false,
|
||||||
|
}) {
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
color: bgColor,
|
||||||
color: Colors.white,
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: [BoxShadow(blurRadius: 5, color: Colors.black26)],
|
boxShadow: isPulsing
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: bgColor.withOpacity(0.4),
|
||||||
|
blurRadius: 12,
|
||||||
|
spreadRadius: 2,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color, size: 24),
|
||||||
),
|
),
|
||||||
child: IconButton(
|
|
||||||
icon: Icon(icon, color: color, size: 28),
|
|
||||||
tooltip: tooltip,
|
|
||||||
onPressed: onPressed,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// الكود الخاص بنافذة إدخال رقم الطوارئ
|
// --- Logic Functions ---
|
||||||
void _handleSosCall(MapDriverController mapDriverController) {
|
void _handleSosCall(MapDriverController mapDriverController) {
|
||||||
if (box.read(BoxName.sosPhoneDriver) == null) {
|
if (box.read(BoxName.sosPhoneDriver) == null) {
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
title: 'Insert Emergency Number'.tr,
|
title: 'Emergency Contact'.tr,
|
||||||
content: Form(
|
content: Column(
|
||||||
|
children: [
|
||||||
|
Text('Please enter the emergency number.'.tr),
|
||||||
|
Form(
|
||||||
key: mapDriverController.formKey1,
|
key: mapDriverController.formKey1,
|
||||||
child: MyTextForm(
|
child: MyTextForm(
|
||||||
controller: mapDriverController.sosEmergincyNumberCotroller,
|
controller: mapDriverController.sosEmergincyNumberCotroller,
|
||||||
label: 'Emergency Number'.tr,
|
label: 'Phone Number'.tr,
|
||||||
hint: 'Enter phone number'.tr,
|
hint: '01xxxxxxxxx',
|
||||||
type: TextInputType.phone,
|
type: TextInputType.phone,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
confirm: MyElevatedButton(
|
confirm: MyElevatedButton(
|
||||||
title: 'Save'.tr,
|
title: 'Save & Call'.tr,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (mapDriverController.formKey1.currentState!.validate()) {
|
if (mapDriverController.formKey1.currentState!.validate()) {
|
||||||
box.write(BoxName.sosPhoneDriver,
|
box.write(BoxName.sosPhoneDriver,
|
||||||
@@ -313,99 +164,70 @@ class SosConnect extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New: الكود الخاص بنافذة الرسائل السريعة (مستخرج من passenger_info_window.dart)
|
|
||||||
void _showMessageOptions(
|
void _showMessageOptions(
|
||||||
BuildContext context, MapDriverController controller) {
|
BuildContext context, MapDriverController controller) {
|
||||||
Get.bottomSheet(
|
Get.bottomSheet(
|
||||||
backgroundColor: Colors.white,
|
Container(
|
||||||
shape: const RoundedRectangleBorder(
|
padding: const EdgeInsets.all(20),
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(25)),
|
||||||
),
|
),
|
||||||
Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: _buildMessageOptions(controller),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMessageOptions(MapDriverController controller) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text('Select a quick message'.tr, style: AppStyle.title),
|
Text('Quick Messages'.tr,
|
||||||
const SizedBox(height: 16),
|
style:
|
||||||
_buildMessageTile(
|
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
text: "Where are you, sir?".tr,
|
const SizedBox(height: 15),
|
||||||
onTap: () {
|
_buildQuickMessageItem("Where are you, sir?".tr, controller),
|
||||||
Get.find<FirebaseMessagesController>()
|
_buildQuickMessageItem("I've arrived.".tr, controller),
|
||||||
.sendNotificationToDriverMAP(
|
const Divider(),
|
||||||
'message From Driver',
|
|
||||||
"Where are you, sir?".tr,
|
|
||||||
controller.tokenPassenger,
|
|
||||||
[],
|
|
||||||
'ding.wav');
|
|
||||||
Get.back();
|
|
||||||
}),
|
|
||||||
_buildMessageTile(
|
|
||||||
text: "I've been trying to reach you but your phone is off.".tr,
|
|
||||||
onTap: () {
|
|
||||||
Get.find<FirebaseMessagesController>()
|
|
||||||
.sendNotificationToDriverMAP(
|
|
||||||
'message From Driver',
|
|
||||||
"I've been trying to reach you but your phone is off.".tr,
|
|
||||||
controller.tokenPassenger,
|
|
||||||
[],
|
|
||||||
'ding.wav');
|
|
||||||
Get.back();
|
|
||||||
}),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Form(
|
child: TextField(
|
||||||
key: controller.formKey2,
|
|
||||||
child: MyTextForm(
|
|
||||||
controller: controller.messageToPassenger,
|
controller: controller.messageToPassenger,
|
||||||
label: 'Type something'.tr,
|
decoration:
|
||||||
hint: 'Type something'.tr,
|
InputDecoration(hintText: 'Type a message...'.tr),
|
||||||
type: TextInputType.text,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
icon: const Icon(Icons.send),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Get.find<FirebaseMessagesController>()
|
_sendMessage(controller, controller.messageToPassenger.text,
|
||||||
.sendNotificationToDriverMAP(
|
'cancel');
|
||||||
'message From Driver',
|
|
||||||
controller.messageToPassenger.text,
|
|
||||||
controller.tokenPassenger,
|
|
||||||
[],
|
|
||||||
'ding.wav');
|
|
||||||
controller.messageToPassenger.clear();
|
controller.messageToPassenger.clear();
|
||||||
Get.back();
|
Get.back();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.send),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMessageTile(
|
Widget _buildQuickMessageItem(String text, MapDriverController controller) {
|
||||||
{required String text, required VoidCallback onTap}) {
|
return ListTile(
|
||||||
return InkWell(
|
title: Text(text),
|
||||||
onTap: onTap,
|
onTap: () {
|
||||||
child: Container(
|
_sendMessage(controller, text, 'ding');
|
||||||
width: double.infinity,
|
Get.back();
|
||||||
padding: const EdgeInsets.all(12),
|
},
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
);
|
||||||
decoration: BoxDecoration(
|
}
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
color: Colors.grey[100],
|
void _sendMessage(MapDriverController controller, String body, String tone) {
|
||||||
),
|
NotificationService.sendNotification(
|
||||||
child: Text(text, style: AppStyle.title),
|
target: controller.tokenPassenger.toString(),
|
||||||
),
|
title: 'Driver Message'.tr,
|
||||||
|
body: body,
|
||||||
|
isTopic: false,
|
||||||
|
tone: tone,
|
||||||
|
driverList: [],
|
||||||
|
category: 'message From Driver',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
76
lib/views/home/Captin/mapDriverWidgets/sped_circle.dart
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../../../../constant/colors.dart';
|
||||||
|
import '../../../../constant/style.dart';
|
||||||
|
import '../../../../controller/home/captin/map_driver_controller.dart';
|
||||||
|
|
||||||
|
// ويدجت للعرض فقط (بدون منطق فتح نوافذ)
|
||||||
|
class SpeedCircle extends StatelessWidget {
|
||||||
|
const SpeedCircle({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GetBuilder<MapDriverController>(
|
||||||
|
id: 'SpeedCircle', // نحدد ID للتحديث الخفيف
|
||||||
|
builder: (controller) {
|
||||||
|
// إذا السرعة 0 أو أقل، نخفي الدائرة
|
||||||
|
if (controller.speed <= 0) return const SizedBox();
|
||||||
|
|
||||||
|
bool isSpeeding = controller.speed > 100;
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
left: 20,
|
||||||
|
top: 100, // مكانها المناسب
|
||||||
|
child: Container(
|
||||||
|
width: 70,
|
||||||
|
height: 70,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.2),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
border: Border.all(
|
||||||
|
color: _getSpeedColor(controller.speed),
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
controller.speed.toStringAsFixed(0),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: AppStyle.title.fontFamily,
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
height: 1.0,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"km/h",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getSpeedColor(double speed) {
|
||||||
|
if (speed < 60) return AppColor.greenColor;
|
||||||
|
if (speed < 100) return Colors.orange;
|
||||||
|
return Colors.red;
|
||||||
|
}
|
||||||
|
}
|
||||||
142
lib/views/home/Captin/orderCaptin/marker_generator.dart
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class MarkerGenerator {
|
||||||
|
// دالة لرسم ماركر يحتوي على نص (للوقت والمسافة)
|
||||||
|
static Future<BitmapDescriptor> createCustomMarkerBitmap({
|
||||||
|
required String title,
|
||||||
|
required String subtitle,
|
||||||
|
required Color color,
|
||||||
|
required IconData iconData,
|
||||||
|
}) async {
|
||||||
|
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
|
||||||
|
final Canvas canvas = Canvas(pictureRecorder);
|
||||||
|
|
||||||
|
// إعدادات القياسات
|
||||||
|
const double width = 220.0;
|
||||||
|
const double height = 110.0;
|
||||||
|
const double circleRadius = 25.0;
|
||||||
|
|
||||||
|
// 1. رسم المربع (Info Box)
|
||||||
|
final Paint paint = Paint()..color = color;
|
||||||
|
final RRect rRect = RRect.fromRectAndRadius(
|
||||||
|
const Rect.fromLTWH(0, 0, width, 60),
|
||||||
|
const Radius.circular(15),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ظل خفيف
|
||||||
|
canvas.drawShadow(Path()..addRRect(rRect), Colors.black, 5.0, true);
|
||||||
|
canvas.drawRRect(rRect, paint);
|
||||||
|
|
||||||
|
// 2. رسم مثلث صغير أسفل المربع (Arrow Tail)
|
||||||
|
final Path path = Path();
|
||||||
|
path.moveTo(width / 2 - 10, 60);
|
||||||
|
path.lineTo(width / 2, 75);
|
||||||
|
path.lineTo(width / 2 + 10, 60);
|
||||||
|
path.close();
|
||||||
|
canvas.drawPath(path, paint);
|
||||||
|
|
||||||
|
// 3. رسم الدائرة (مكان الأيقونة)
|
||||||
|
canvas.drawCircle(const Offset(width / 2, 85), circleRadius, paint);
|
||||||
|
|
||||||
|
// 4. رسم الأيقونة داخل الدائرة
|
||||||
|
TextPainter iconPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||||
|
iconPainter.text = TextSpan(
|
||||||
|
text: String.fromCharCode(iconData.codePoint),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 30.0,
|
||||||
|
fontFamily: iconData.fontFamily,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
iconPainter.layout();
|
||||||
|
iconPainter.paint(
|
||||||
|
canvas,
|
||||||
|
Offset((width - iconPainter.width) / 2, 85 - (iconPainter.height / 2)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. رسم النصوص (العنوان والوصف) داخل المربع
|
||||||
|
// العنوان (مثلاً: المدة)
|
||||||
|
TextPainter titlePainter = TextPainter(
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
);
|
||||||
|
titlePainter.text = TextSpan(
|
||||||
|
text: title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20.0,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
titlePainter.layout(minWidth: width);
|
||||||
|
titlePainter.paint(canvas, const Offset(0, 8));
|
||||||
|
|
||||||
|
// الوصف (مثلاً: المسافة)
|
||||||
|
TextPainter subTitlePainter = TextPainter(
|
||||||
|
textDirection: TextDirection.rtl,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
);
|
||||||
|
subTitlePainter.text = TextSpan(
|
||||||
|
text: subtitle,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16.0,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
subTitlePainter.layout(minWidth: width);
|
||||||
|
subTitlePainter.paint(canvas, const Offset(0, 32));
|
||||||
|
|
||||||
|
// تحويل الرسم إلى صورة
|
||||||
|
final ui.Image image = await pictureRecorder.endRecording().toImage(
|
||||||
|
width.toInt(),
|
||||||
|
(height + 20).toInt(), // مساحة إضافية
|
||||||
|
);
|
||||||
|
final ByteData? data =
|
||||||
|
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
return BitmapDescriptor.fromBytes(data!.buffer.asUint8List());
|
||||||
|
}
|
||||||
|
|
||||||
|
// دالة خاصة لرسم ماركر السائق (دائرة وخلفها سهم)
|
||||||
|
static Future<BitmapDescriptor> createDriverMarker() async {
|
||||||
|
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
|
||||||
|
final Canvas canvas = Canvas(pictureRecorder);
|
||||||
|
const double size = 100.0;
|
||||||
|
|
||||||
|
final Paint paint = Paint()..color = const Color(0xFF2E7D32); // أخضر غامق
|
||||||
|
final Paint borderPaint = Paint()
|
||||||
|
..color = Colors.white
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 4.0;
|
||||||
|
|
||||||
|
// الدائرة
|
||||||
|
canvas.drawCircle(const Offset(size / 2, size / 2), size / 2.5, paint);
|
||||||
|
canvas.drawCircle(
|
||||||
|
const Offset(size / 2, size / 2), size / 2.5, borderPaint);
|
||||||
|
|
||||||
|
// رسم السهم (Arrow Up)
|
||||||
|
TextPainter iconPainter = TextPainter(textDirection: TextDirection.ltr);
|
||||||
|
iconPainter.text = TextSpan(
|
||||||
|
text: String.fromCharCode(Icons.navigation.codePoint), // سهم ملاحة
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 40.0,
|
||||||
|
fontFamily: Icons.navigation.fontFamily,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
iconPainter.layout();
|
||||||
|
iconPainter.paint(
|
||||||
|
canvas,
|
||||||
|
Offset((size - iconPainter.width) / 2, (size - iconPainter.height) / 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
final ui.Image image = await pictureRecorder
|
||||||
|
.endRecording()
|
||||||
|
.toImage(size.toInt(), size.toInt());
|
||||||
|
final ByteData? data =
|
||||||
|
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
return BitmapDescriptor.fromBytes(data!.buffer.asUint8List());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
import 'package:flutter_overlay_window/flutter_overlay_window.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
import 'package:sefer_driver/constant/api_key.dart';
|
import 'package:sefer_driver/constant/api_key.dart';
|
||||||
|
import 'package:sefer_driver/models/overlay_service.dart';
|
||||||
import '../../../../constant/box_name.dart';
|
import '../../../../constant/box_name.dart';
|
||||||
import '../../../../constant/links.dart';
|
import '../../../../constant/links.dart';
|
||||||
import '../../../../controller/firebase/firbase_messge.dart';
|
import '../../../../controller/firebase/firbase_messge.dart';
|
||||||
import '../../../../controller/firebase/local_notification.dart';
|
import '../../../../controller/firebase/local_notification.dart';
|
||||||
|
import '../../../../controller/firebase/notification_service.dart';
|
||||||
import '../../../../controller/functions/crud.dart';
|
import '../../../../controller/functions/crud.dart';
|
||||||
import '../../../../main.dart';
|
import '../../../../main.dart';
|
||||||
import '../../../../models/model/order_data.dart';
|
import '../../../../models/model/order_data.dart';
|
||||||
@@ -212,29 +216,15 @@ class _OrderOverlayState extends State<OrderOverlay>
|
|||||||
await _closeOverlay();
|
await _closeOverlay();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var res = await CRUD()
|
||||||
var res = await CRUD().post(link: AppLink.updateStausFromSpeed, payload: {
|
.post(link: "${AppLink.ride}/rides/acceptRide.php", payload: {
|
||||||
'id': orderData!.orderId,
|
'id': orderData!.orderId,
|
||||||
'rideTimeStart': DateTime.now().toString(),
|
'rideTimeStart': DateTime.now().toString(),
|
||||||
'status': 'Apply',
|
'status': 'Apply',
|
||||||
'driver_id': box.read(BoxName.driverID),
|
'driver_id': box.read(BoxName.driverID),
|
||||||
|
'passengerToken': _getData(9),
|
||||||
});
|
});
|
||||||
List<String> bodyToPassenger = [
|
|
||||||
_getData(6).toString(),
|
|
||||||
_getData(8).toString(),
|
|
||||||
_getData(9).toString(),
|
|
||||||
];
|
|
||||||
final fmc = Get.isRegistered<FirebaseMessagesController>()
|
|
||||||
? Get.find<FirebaseMessagesController>()
|
|
||||||
: Get.put(FirebaseMessagesController());
|
|
||||||
|
|
||||||
fmc.sendNotificationToDriverMAP(
|
|
||||||
"Accepted Ride",
|
|
||||||
'your ride is Accepted'.tr,
|
|
||||||
_getData(9).toString(),
|
|
||||||
bodyToPassenger,
|
|
||||||
'start.wav',
|
|
||||||
);
|
|
||||||
final payload = {
|
final payload = {
|
||||||
// بيانات أساسية
|
// بيانات أساسية
|
||||||
'driver_id': driverId,
|
'driver_id': driverId,
|
||||||
@@ -271,7 +261,6 @@ class _OrderOverlayState extends State<OrderOverlay>
|
|||||||
'timeOfOrder': DateTime.now().toIso8601String(),
|
'timeOfOrder': DateTime.now().toIso8601String(),
|
||||||
'totalPassenger': _getData(2),
|
'totalPassenger': _getData(2),
|
||||||
};
|
};
|
||||||
Log.print('myList: ${myList}');
|
|
||||||
Log.print('payload: ${payload}');
|
Log.print('payload: ${payload}');
|
||||||
CRUD().post(
|
CRUD().post(
|
||||||
link: AppLink.addOverLayStatus,
|
link: AppLink.addOverLayStatus,
|
||||||
@@ -279,21 +268,7 @@ class _OrderOverlayState extends State<OrderOverlay>
|
|||||||
);
|
);
|
||||||
if (res != "failure") {
|
if (res != "failure") {
|
||||||
// Using rideId (_getData(16)) for order_id consistently
|
// Using rideId (_getData(16)) for order_id consistently
|
||||||
CRUD().post(link: AppLink.addDriverOrder, payload: {
|
|
||||||
'driver_id': driverId, // Driver ID from the order data
|
|
||||||
'order_id': orderData!.orderId,
|
|
||||||
'status': 'Apply'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (AppLink.endPoint != AppLink.seferCairoServer) {
|
|
||||||
CRUD().post(
|
|
||||||
link: "${AppLink.endPoint}/ride/driver_order/add.php",
|
|
||||||
payload: {
|
|
||||||
'driver_id': driverId,
|
|
||||||
'order_id': orderData!.orderId,
|
|
||||||
'status': 'Apply'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
_log("Server update successful. Writing to storage.");
|
_log("Server update successful. Writing to storage.");
|
||||||
notificationController.showNotification(
|
notificationController.showNotification(
|
||||||
"Order Accepted".tr,
|
"Order Accepted".tr,
|
||||||
@@ -301,6 +276,14 @@ class _OrderOverlayState extends State<OrderOverlay>
|
|||||||
'ding',
|
'ding',
|
||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 3. الخطوة الأهم: فتح التطبيق وإغلاق النافذة
|
||||||
|
try {
|
||||||
|
// استدعاء الميثود التي تم تحديثها في الخطوة 1
|
||||||
|
await OverlayMethodChannel.bringToForeground();
|
||||||
|
} catch (e) {
|
||||||
|
_log("Failed to bring app to foreground: $e");
|
||||||
|
}
|
||||||
await _closeOverlay();
|
await _closeOverlay();
|
||||||
} else {
|
} else {
|
||||||
_log("Failed to update order status on server: $res");
|
_log("Failed to update order status on server: $res");
|
||||||
@@ -316,6 +299,12 @@ class _OrderOverlayState extends State<OrderOverlay>
|
|||||||
_log(
|
_log(
|
||||||
"A critical error occurred during server update: $e\nStackTrace: $s");
|
"A critical error occurred during server update: $e\nStackTrace: $s");
|
||||||
if (mounted) setState(() => buttonsEnabled = true);
|
if (mounted) setState(() => buttonsEnabled = true);
|
||||||
|
|
||||||
|
_log("Error in accept order: $e");
|
||||||
|
await _closeOverlay();
|
||||||
|
// حتى في حال الخطأ، نحاول فتح التطبيق ليرى السائق ما حدث
|
||||||
|
await OverlayMethodChannel.bringToForeground();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,16 +338,12 @@ class _OrderOverlayState extends State<OrderOverlay>
|
|||||||
_log("Driver ID is null, cannot refuse order");
|
_log("Driver ID is null, cannot refuse order");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await _crud.post(link: AppLink.addDriverOrder, payload: {
|
CRUD().post(link: AppLink.addDriverOrder, payload: {
|
||||||
'driver_id': driverId,
|
'driver_id': driverId,
|
||||||
'order_id': orderID,
|
'order_id': orderID,
|
||||||
'status': 'Refused'
|
'status': 'Refused'
|
||||||
});
|
});
|
||||||
await _crud.post(link: AppLink.updateRides, payload: {
|
|
||||||
'id': orderID,
|
|
||||||
'status': 'Refused',
|
|
||||||
'driver_id': driverId,
|
|
||||||
});
|
|
||||||
_log("Order $orderID refused successfully");
|
_log("Order $orderID refused successfully");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log("Error in _apiRefuseOrder for $orderID: $e");
|
_log("Error in _apiRefuseOrder for $orderID: $e");
|
||||||
@@ -503,6 +488,11 @@ class _OrderOverlayState extends State<OrderOverlay>
|
|||||||
|
|
||||||
Widget _buildPrimaryInfo() {
|
Widget _buildPrimaryInfo() {
|
||||||
final order = orderData!;
|
final order = orderData!;
|
||||||
|
|
||||||
|
// FIX: Parse the price to a number safely before formatting
|
||||||
|
// This handles cases where order.price is a String like "173"
|
||||||
|
final num priceValue = num.tryParse(order.price.toString()) ?? 0;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -518,15 +508,21 @@ class _OrderOverlayState extends State<OrderOverlay>
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: _buildHighlightInfo("\$${order.price}", "السعر".tr,
|
child: _buildHighlightInfo(
|
||||||
Icons.monetization_on_rounded, AppColors.priceHighlight,
|
// FIX: Use the parsed priceValue here
|
||||||
isLarge: true),
|
"${NumberFormat('#,##0').format(priceValue)} ل.س",
|
||||||
|
"السعر".tr,
|
||||||
|
Icons.monetization_on_rounded,
|
||||||
|
AppColors.priceHighlight,
|
||||||
|
isLarge: true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: _buildHighlightInfo(
|
child: _buildHighlightInfo(
|
||||||
"${order.tripDistanceKm.toStringAsFixed(1)} كم",
|
// Ensure tripDistanceKm is treated safely too
|
||||||
|
"${(num.tryParse(order.tripDistanceKm.toString()) ?? 0).toStringAsFixed(1)} كم",
|
||||||
"المسافة".tr,
|
"المسافة".tr,
|
||||||
Icons.straighten_rounded,
|
Icons.straighten_rounded,
|
||||||
AppColors.accent,
|
AppColors.accent,
|
||||||
|
|||||||
@@ -1,435 +1,411 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:sefer_driver/controller/home/captin/home_captain_controller.dart';
|
|
||||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sefer_driver/constant/box_name.dart';
|
|
||||||
import 'package:sefer_driver/controller/firebase/firbase_messge.dart';
|
|
||||||
import 'package:sefer_driver/main.dart';
|
|
||||||
import 'package:sefer_driver/views/home/Captin/driver_map_page.dart';
|
|
||||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
import 'dart:math' as math;
|
import 'package:sefer_driver/constant/colors.dart';
|
||||||
import '../../../../constant/colors.dart';
|
import 'package:sefer_driver/controller/home/captin/order_request_controller.dart';
|
||||||
import '../../../../constant/links.dart';
|
|
||||||
import '../../../../constant/style.dart';
|
|
||||||
import '../../../../controller/functions/crud.dart';
|
|
||||||
import '../../../../controller/functions/encrypt_decrypt.dart';
|
|
||||||
import '../../../../controller/functions/launch.dart';
|
|
||||||
import '../../../../controller/home/captin/order_request_controller.dart';
|
|
||||||
import '../../../widgets/elevated_btn.dart';
|
|
||||||
|
|
||||||
class OrderRequestPage extends StatefulWidget {
|
class OrderRequestPage extends StatelessWidget {
|
||||||
const OrderRequestPage({super.key});
|
const OrderRequestPage({super.key});
|
||||||
|
|
||||||
@override
|
|
||||||
State<OrderRequestPage> createState() => _OrderRequestPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _OrderRequestPageState extends State<OrderRequestPage> {
|
|
||||||
final OrderRequestController orderRequestController =
|
|
||||||
Get.put(OrderRequestController());
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// حقن الكنترولر
|
||||||
|
final OrderRequestController controller = Get.put(OrderRequestController());
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
body: Directionality(
|
||||||
title: Text('Order Request'.tr),
|
textDirection: TextDirection.rtl,
|
||||||
centerTitle: true,
|
child: GetBuilder<OrderRequestController>(
|
||||||
),
|
|
||||||
body: GetBuilder<OrderRequestController>(
|
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
if (controller.myList == null) {
|
// 🔥 التعديل الأهم: التحقق من وجود أي بيانات (List أو Map)
|
||||||
return const Center(child: CircularProgressIndicator());
|
if (controller.myList == null && controller.myMapData == null) {
|
||||||
|
return const Center(
|
||||||
|
child:
|
||||||
|
CircularProgressIndicator()); // شاشة تحميل بدلاً من فراغ
|
||||||
}
|
}
|
||||||
return Column(
|
|
||||||
|
// 🔥 استخدام دوال الكنترولر الآمنة لجلب البيانات بدلاً من الوصول المباشر
|
||||||
|
// قمت بتحويل _safeGet إلى دالة عامة safeGet في الكنترولر (تأكد من جعلها public)
|
||||||
|
// أو سأقوم بكتابة المنطق هنا مباشرة لضمان العمل:
|
||||||
|
|
||||||
|
String getValue(int index) {
|
||||||
|
if (controller.myList != null &&
|
||||||
|
index < controller.myList!.length) {
|
||||||
|
return controller.myList![index].toString();
|
||||||
|
}
|
||||||
|
if (controller.myMapData != null &&
|
||||||
|
controller.myMapData!.containsKey(index.toString())) {
|
||||||
|
return controller.myMapData![index.toString()].toString();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
final String passengerName =
|
||||||
|
getValue(8).isEmpty ? "عميل" : getValue(8);
|
||||||
|
final String startAddr =
|
||||||
|
getValue(29).isEmpty ? "موقع الانطلاق" : getValue(29);
|
||||||
|
final String endAddr =
|
||||||
|
getValue(30).isEmpty ? "الوجهة" : getValue(30);
|
||||||
|
final bool isVisa = (getValue(13) == 'true');
|
||||||
|
|
||||||
|
// منطق Speed = سعر ثابت
|
||||||
|
final bool isSpeed =
|
||||||
|
controller.tripType.toLowerCase().contains('speed');
|
||||||
|
final String carTypeLabel =
|
||||||
|
isSpeed ? "سعر ثابت" : controller.tripType;
|
||||||
|
final Color carTypeColor =
|
||||||
|
isSpeed ? Colors.red.shade700 : Colors.blue.shade700;
|
||||||
|
final IconData carIcon =
|
||||||
|
isSpeed ? Icons.local_offer : Icons.directions_car;
|
||||||
|
|
||||||
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
// 1. الخارطة
|
||||||
height: Get.height * 0.3,
|
Positioned.fill(
|
||||||
|
bottom: 300,
|
||||||
child: GoogleMap(
|
child: GoogleMap(
|
||||||
mapType: MapType.normal,
|
mapType: MapType.normal,
|
||||||
initialCameraPosition: CameraPosition(
|
initialCameraPosition: CameraPosition(
|
||||||
target: LatLng(controller.latPassengerLocation,
|
target: LatLng(
|
||||||
controller.lngPassengerLocation),
|
controller.latPassenger, controller.lngPassenger),
|
||||||
zoom: 14.0,
|
zoom: 13.0,
|
||||||
),
|
|
||||||
myLocationButtonEnabled: true,
|
|
||||||
onMapCreated: controller.onMapCreated,
|
|
||||||
myLocationEnabled: true,
|
|
||||||
markers: {
|
|
||||||
Marker(
|
|
||||||
markerId: const MarkerId('startLocation'),
|
|
||||||
position: LatLng(controller.latPassengerLocation,
|
|
||||||
controller.lngPassengerLocation),
|
|
||||||
icon: controller.startIcon,
|
|
||||||
),
|
|
||||||
Marker(
|
|
||||||
markerId: const MarkerId('destinationLocation'),
|
|
||||||
position: LatLng(controller.latPassengerDestination,
|
|
||||||
controller.lngPassengerDestination),
|
|
||||||
icon: controller.endIcon,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
polylines: {
|
|
||||||
Polyline(
|
|
||||||
polylineId: const PolylineId('route'),
|
|
||||||
color: AppColor.primaryColor,
|
|
||||||
width: 5,
|
|
||||||
points: controller.pointsDirection,
|
|
||||||
),
|
),
|
||||||
|
markers: controller.markers,
|
||||||
|
polylines: controller.polylines,
|
||||||
|
zoomControlsEnabled: false,
|
||||||
|
myLocationButtonEnabled: false,
|
||||||
|
compassEnabled: false,
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 80, bottom: 20, left: 20, right: 20),
|
||||||
|
onMapCreated: (c) {
|
||||||
|
controller.onMapCreated(c);
|
||||||
|
controller.update();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
|
||||||
child: ListView(
|
// 2. كبسولة الوصول للراكب
|
||||||
padding: const EdgeInsets.all(16),
|
Positioned(
|
||||||
|
top: 50,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 15, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black87,
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black26, blurRadius: 8)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Card(
|
const Icon(Icons.near_me,
|
||||||
elevation: 4,
|
color: Colors.amber, size: 16),
|
||||||
child: ListTile(
|
const SizedBox(width: 8),
|
||||||
leading: Icon(
|
|
||||||
controller.myList[13].toString() == 'true'
|
|
||||||
? Icons.credit_card
|
|
||||||
: Icons.money,
|
|
||||||
color: controller.myList[13].toString() == 'true'
|
|
||||||
? AppColor.deepPurpleAccent
|
|
||||||
: AppColor.greenColor,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
'Payment Method'.tr,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
trailing: Text(
|
|
||||||
controller.myList[13].toString() == 'true'
|
|
||||||
? 'Visa'
|
|
||||||
: 'Cash',
|
|
||||||
style:
|
|
||||||
Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Card(
|
|
||||||
elevation: 4,
|
|
||||||
child: ListTile(
|
|
||||||
leading: const Icon(Icons.account_circle,
|
|
||||||
color: AppColor.secondaryColor),
|
|
||||||
title: Text(
|
|
||||||
controller.myList[8],
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
subtitle: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.star,
|
|
||||||
size: 16, color: Colors.amber),
|
|
||||||
Text(
|
Text(
|
||||||
controller.myList[33].toString(),
|
"الوصول للراكب: ${controller.timeToPassenger}",
|
||||||
style: const TextStyle(color: Colors.amber),
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
Card(
|
|
||||||
elevation: 4,
|
// 3. البطاقة السفلية
|
||||||
child: Padding(
|
Align(
|
||||||
padding: const EdgeInsets.all(16.0),
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Container(
|
||||||
|
height: 360,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(25),
|
||||||
|
topRight: Radius.circular(25),
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black12,
|
||||||
|
blurRadius: 20,
|
||||||
|
spreadRadius: 5)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
borderRadius: BorderRadius.circular(2)))),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// الصف الأول: الراكب والسعر
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.location_on,
|
const CircleAvatar(
|
||||||
color: AppColor.greenColor),
|
radius: 24,
|
||||||
const SizedBox(width: 8),
|
backgroundColor: Color(0xFFF5F5F5),
|
||||||
Expanded(
|
child: Icon(Icons.person,
|
||||||
// Keep Expanded here for layout
|
color: Colors.grey, size: 28),
|
||||||
child: Text(
|
|
||||||
controller.myList[29],
|
|
||||||
style:
|
|
||||||
Theme.of(context).textTheme.titleSmall,
|
|
||||||
maxLines: 2, // Allow up to 2 lines
|
|
||||||
overflow: TextOverflow
|
|
||||||
.ellipsis, // Handle overflow
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 10),
|
||||||
],
|
Column(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
const Divider(),
|
children: [
|
||||||
|
Text(passengerName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 17,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.flag,
|
const Icon(Icons.star,
|
||||||
color: AppColor.redColor),
|
color: Colors.amber, size: 14),
|
||||||
const SizedBox(width: 8),
|
Text(controller.passengerRating,
|
||||||
Expanded(
|
style: const TextStyle(
|
||||||
// Keep Expanded here for layout
|
fontSize: 13,
|
||||||
child: Text(
|
fontWeight: FontWeight.bold)),
|
||||||
controller.myList[30],
|
],
|
||||||
style:
|
),
|
||||||
Theme.of(context).textTheme.titleSmall,
|
],
|
||||||
maxLines: 2, // Allow up to 2 lines
|
),
|
||||||
overflow: TextOverflow
|
],
|
||||||
.ellipsis, // Handle overflow
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text("${controller.tripPrice} ل.س",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColor.primaryColor)),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isVisa
|
||||||
|
? Colors.purple.withOpacity(0.1)
|
||||||
|
: Colors.green.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(4)),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(isVisa ? "فيزا" : "كاش",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isVisa
|
||||||
|
? Colors.purple
|
||||||
|
: Colors.green)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Icon(
|
||||||
|
isVisa
|
||||||
|
? Icons.credit_card
|
||||||
|
: Icons.money,
|
||||||
|
size: 14,
|
||||||
|
color: isVisa
|
||||||
|
? Colors.purple
|
||||||
|
: Colors.green),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// الصف الثاني: شريط المعلومات
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 10, horizontal: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF8F9FA),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
Card(
|
|
||||||
elevation: 4,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
_InfoTile(
|
_buildInfoItem(
|
||||||
icon: Icons.timer,
|
carIcon, carTypeLabel, carTypeColor),
|
||||||
label:
|
Container(
|
||||||
'${(double.parse(controller.myList[12]) / 60).toStringAsFixed(0)} ${'min'.tr}',
|
height: 20,
|
||||||
),
|
width: 1,
|
||||||
_InfoTile(
|
color: Colors.grey.shade300),
|
||||||
icon: Icons.directions_car,
|
_buildInfoItem(Icons.route,
|
||||||
label:
|
controller.totalTripDistance, Colors.black87),
|
||||||
'${(double.parse(controller.myList[11]) / 1000).toStringAsFixed(1)} ${'km'.tr}',
|
Container(
|
||||||
),
|
height: 20,
|
||||||
_InfoTile(
|
width: 1,
|
||||||
icon: Icons.monetization_on,
|
color: Colors.grey.shade300),
|
||||||
label: '${controller.myList[2]}',
|
_buildInfoItem(Icons.access_time_filled,
|
||||||
),
|
controller.totalTripDuration, Colors.black87),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// الصف الثالث: العناوين
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.my_location,
|
||||||
|
size: 18, color: Colors.green),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
width: 2,
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 2))),
|
||||||
|
const Icon(Icons.location_on,
|
||||||
|
size: 18, color: Colors.red),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text("موقع الانطلاق",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey)),
|
||||||
|
Text(startAddr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text("الوجهة",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey)),
|
||||||
|
Text(endAddr,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// الصف الرابع: الأزرار
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
||||||
children: [
|
children: [
|
||||||
MyElevatedButton(
|
InkWell(
|
||||||
kolor: AppColor.greenColor,
|
onTap: () => Get.back(),
|
||||||
title: 'Accept Order'.tr,
|
child: Container(
|
||||||
onPressed: () async {
|
height: 50,
|
||||||
Get.put(HomeCaptainController()).changeRideId();
|
width: 50,
|
||||||
box.write(BoxName.statusDriverLocation, 'on');
|
decoration: BoxDecoration(
|
||||||
controller.endTimer();
|
color: Colors.red.shade50,
|
||||||
controller.changeApplied();
|
shape: BoxShape.circle,
|
||||||
|
border:
|
||||||
var res = await CRUD().post(
|
Border.all(color: Colors.red.shade100)),
|
||||||
link: AppLink.updateStausFromSpeed,
|
child: const Icon(Icons.close,
|
||||||
payload: {
|
color: Colors.red, size: 24),
|
||||||
'id': (controller.myList[16]),
|
|
||||||
'rideTimeStart': DateTime.now().toString(),
|
|
||||||
'status': 'Apply',
|
|
||||||
'driver_id': box.read(BoxName.driverID),
|
|
||||||
});
|
|
||||||
if (AppLink.endPoint != AppLink.seferCairoServer) {
|
|
||||||
CRUD().post(
|
|
||||||
link:
|
|
||||||
"${AppLink.endPoint}/ride/rides/updateStausFromSpeed.php",
|
|
||||||
payload: {
|
|
||||||
'id': (controller.myList[16]),
|
|
||||||
'rideTimeStart': DateTime.now().toString(),
|
|
||||||
'status': 'Apply',
|
|
||||||
'driver_id': box.read(BoxName.driverID),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (res == 'failure') {
|
|
||||||
MyDialog().getDialog(
|
|
||||||
"This ride is already applied by another driver."
|
|
||||||
.tr,
|
|
||||||
'', () {
|
|
||||||
Get.back();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await CRUD().postFromDialogue(
|
|
||||||
link: AppLink.addDriverOrder,
|
|
||||||
payload: {
|
|
||||||
'driver_id':
|
|
||||||
(controller.myList[6].toString()),
|
|
||||||
'order_id':
|
|
||||||
(controller.myList[16].toString()),
|
|
||||||
'status': 'Apply'
|
|
||||||
});
|
|
||||||
if (AppLink.endPoint !=
|
|
||||||
AppLink.seferCairoServer) {
|
|
||||||
CRUD().postFromDialogue(
|
|
||||||
link:
|
|
||||||
'${AppLink.endPoint}/rides/driver_order/add.php',
|
|
||||||
payload: {
|
|
||||||
'driver_id':
|
|
||||||
(controller.myList[6].toString()),
|
|
||||||
'order_id':
|
|
||||||
(controller.myList[16].toString()),
|
|
||||||
'status': 'Apply'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
List<String> bodyToPassenger = [
|
|
||||||
controller.myList[6].toString(),
|
|
||||||
controller.myList[8].toString(),
|
|
||||||
controller.myList[9].toString(),
|
|
||||||
];
|
|
||||||
final fmc =
|
|
||||||
Get.isRegistered<FirebaseMessagesController>()
|
|
||||||
? Get.find<FirebaseMessagesController>()
|
|
||||||
: Get.put(FirebaseMessagesController());
|
|
||||||
|
|
||||||
fmc.sendNotificationToDriverMAP(
|
|
||||||
"Accepted Ride",
|
|
||||||
'your ride is Accepted'.tr,
|
|
||||||
controller.myList[9].toString(),
|
|
||||||
bodyToPassenger,
|
|
||||||
'start.wav',
|
|
||||||
);
|
|
||||||
Get.back();
|
|
||||||
box.write(BoxName.rideArguments, {
|
|
||||||
'passengerLocation':
|
|
||||||
controller.myList[0].toString(),
|
|
||||||
'passengerDestination':
|
|
||||||
controller.myList[1].toString(),
|
|
||||||
'Duration': controller.myList[4].toString(),
|
|
||||||
'totalCost': controller.myList[26].toString(),
|
|
||||||
'Distance': controller.myList[5].toString(),
|
|
||||||
'name': controller.myList[8].toString(),
|
|
||||||
'phone': controller.myList[10].toString(),
|
|
||||||
'email': controller.myList[28].toString(),
|
|
||||||
'WalletChecked':
|
|
||||||
controller.myList[13].toString(),
|
|
||||||
'tokenPassenger':
|
|
||||||
controller.myList[9].toString(),
|
|
||||||
'direction':
|
|
||||||
'https://www.google.com/maps/dir/${controller.myList[0]}/${controller.myList[1]}/',
|
|
||||||
'DurationToPassenger':
|
|
||||||
controller.myList[15].toString(),
|
|
||||||
'rideId': (controller.myList[16].toString()),
|
|
||||||
'passengerId':
|
|
||||||
(controller.myList[7].toString()),
|
|
||||||
'driverId': (controller.myList[18].toString()),
|
|
||||||
'durationOfRideValue':
|
|
||||||
controller.myList[19].toString(),
|
|
||||||
'paymentAmount':
|
|
||||||
controller.myList[2].toString(),
|
|
||||||
'paymentMethod':
|
|
||||||
controller.myList[13].toString() == 'true'
|
|
||||||
? 'visa'
|
|
||||||
: 'cash',
|
|
||||||
'isHaveSteps': controller.myList[20].toString(),
|
|
||||||
'step0': controller.myList[21].toString(),
|
|
||||||
'step1': controller.myList[22].toString(),
|
|
||||||
'step2': controller.myList[23].toString(),
|
|
||||||
'step3': controller.myList[24].toString(),
|
|
||||||
'step4': controller.myList[25].toString(),
|
|
||||||
'passengerWalletBurc':
|
|
||||||
controller.myList[26].toString(),
|
|
||||||
'timeOfOrder': DateTime.now().toString(),
|
|
||||||
'totalPassenger':
|
|
||||||
controller.myList[2].toString(),
|
|
||||||
'carType': controller.myList[31].toString(),
|
|
||||||
'kazan': controller.myList[32].toString(),
|
|
||||||
'startNameLocation':
|
|
||||||
controller.myList[29].toString(),
|
|
||||||
'endNameLocation':
|
|
||||||
controller.myList[30].toString(),
|
|
||||||
});
|
|
||||||
Get.to(() => PassengerLocationMapPage(),
|
|
||||||
arguments: box.read(BoxName.rideArguments));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
GetBuilder<OrderRequestController>(
|
),
|
||||||
builder: (timerController) {
|
const SizedBox(width: 15),
|
||||||
final isNearEnd = timerController.remainingTime <=
|
Expanded(
|
||||||
5; // Define a threshold for "near end"
|
child: ElevatedButton(
|
||||||
|
onPressed: () => controller.acceptOrder(),
|
||||||
return Stack(
|
style: ElevatedButton.styleFrom(
|
||||||
alignment: Alignment.center,
|
backgroundColor: AppColor.primaryColor,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(15)),
|
||||||
|
elevation: 2,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(
|
const Text("قبول الرحلة",
|
||||||
value: timerController.progress,
|
style: TextStyle(
|
||||||
// Set the color based on the "isNearEnd" condition
|
fontSize: 16,
|
||||||
color: isNearEnd ? Colors.red : Colors.blue,
|
fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: controller.progress,
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2.5,
|
||||||
|
backgroundColor: Colors.white24),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text("${controller.remainingTime}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14, color: Colors.white)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
|
||||||
'${timerController.remainingTime}',
|
|
||||||
style: AppStyle.number,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
MyElevatedButton(
|
|
||||||
title: 'Refuse Order'.tr,
|
|
||||||
onPressed: () async {
|
|
||||||
controller.endTimer();
|
|
||||||
List<String> bodyToPassenger = [
|
|
||||||
box.read(BoxName.driverID).toString(),
|
|
||||||
box.read(BoxName.nameDriver).toString(),
|
|
||||||
box.read(BoxName.tokenDriver).toString(),
|
|
||||||
];
|
|
||||||
|
|
||||||
FirebaseMessagesController()
|
|
||||||
.sendNotificationToPassengerToken(
|
|
||||||
'Order Under Review'.tr,
|
|
||||||
'${box.read(BoxName.nameDriver)} ${'is reviewing your order. They may need more information or a higher price.'.tr}',
|
|
||||||
controller.myList[9].toString(),
|
|
||||||
bodyToPassenger,
|
|
||||||
'notification.wav');
|
|
||||||
|
|
||||||
controller.refuseOrder(
|
|
||||||
EncryptionHelper.instance.encryptData(
|
|
||||||
controller.myList[16].toString()),
|
|
||||||
);
|
|
||||||
controller.addRideToNotificationDriverString(
|
|
||||||
controller.myList[16].toString(),
|
|
||||||
controller.myList[29].toString(),
|
|
||||||
controller.myList[30].toString(),
|
|
||||||
'${DateTime.now().year}-${DateTime.now().month}-${DateTime.now().day}',
|
|
||||||
'${DateTime.now().hour}:${DateTime.now().minute}',
|
|
||||||
controller.myList[2].toString(),
|
|
||||||
controller.myList[7].toString(),
|
|
||||||
'wait',
|
|
||||||
controller.myList[31].toString(),
|
|
||||||
controller.myList[33].toString(),
|
|
||||||
controller.myList[2].toString(),
|
|
||||||
controller.myList[5].toString(),
|
|
||||||
controller.myList[4].toString());
|
|
||||||
},
|
|
||||||
kolor: AppColor.redColor,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class _InfoTile extends StatelessWidget {
|
Widget _buildInfoItem(IconData icon, String text, Color color) {
|
||||||
final IconData icon;
|
return Row(
|
||||||
final String label;
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
||||||
const _InfoTile({required this.icon, required this.label});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: AppColor.primaryColor),
|
Icon(icon, size: 16, color: color),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(text,
|
||||||
label,
|
style: TextStyle(
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
fontSize: 13, fontWeight: FontWeight.bold, color: color)),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'package:webview_flutter/webview_flutter.dart';
|
|||||||
import '../../../constant/box_name.dart';
|
import '../../../constant/box_name.dart';
|
||||||
import '../../../constant/links.dart';
|
import '../../../constant/links.dart';
|
||||||
import '../../../controller/functions/crud.dart';
|
import '../../../controller/functions/crud.dart';
|
||||||
|
import '../../../controller/payment/mtn_new/mtn_payment_new_screen.dart';
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
import '../../../print.dart';
|
import '../../../print.dart';
|
||||||
import '../../widgets/elevated_btn.dart';
|
import '../../widgets/elevated_btn.dart';
|
||||||
@@ -65,48 +66,49 @@ class PointsCaptain extends StatelessWidget {
|
|||||||
color: AppColor.blueColor, size: 70),
|
color: AppColor.blueColor, size: 70),
|
||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
GestureDetector(
|
// GestureDetector(
|
||||||
onTap: () async {
|
// onTap: () async {
|
||||||
Get.back();
|
// Get.back();
|
||||||
Get.defaultDialog(
|
// Get.defaultDialog(
|
||||||
barrierDismissible: false,
|
// barrierDismissible: false,
|
||||||
title: 'Insert Wallet phone number'.tr,
|
// title: 'Insert Wallet phone number'.tr,
|
||||||
content: Form(
|
// content: Form(
|
||||||
key: paymentController.formKey,
|
// key: paymentController.formKey,
|
||||||
child: MyTextForm(
|
// child: MyTextForm(
|
||||||
controller:
|
// controller:
|
||||||
paymentController.walletphoneController,
|
// paymentController.walletphoneController,
|
||||||
label: 'Insert Wallet phone number'.tr,
|
// label: 'Insert Wallet phone number'.tr,
|
||||||
hint: '963941234567',
|
// hint: '963941234567',
|
||||||
type: TextInputType.phone)),
|
// type: TextInputType.phone)),
|
||||||
confirm: MyElevatedButton(
|
// confirm: MyElevatedButton(
|
||||||
title: 'OK'.tr,
|
// title: 'OK'.tr,
|
||||||
onPressed: () async {
|
// onPressed: () async {
|
||||||
Get.back();
|
// Get.back();
|
||||||
if (paymentController.formKey.currentState!
|
// if (paymentController.formKey.currentState!
|
||||||
.validate()) {
|
// .validate()) {
|
||||||
box.write(
|
// box.write(
|
||||||
BoxName.phoneWallet,
|
// BoxName.phoneWallet,
|
||||||
paymentController
|
// paymentController
|
||||||
.walletphoneController.text);
|
// .walletphoneController.text);
|
||||||
await payWithMTNWallet(
|
// await payWithMTNWallet(
|
||||||
context, pricePoint.toString(), 'SYP');
|
// context, pricePoint.toString(), 'SYP');
|
||||||
}
|
// }
|
||||||
}));
|
// }));
|
||||||
},
|
// },
|
||||||
child: Row(
|
// child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
// children: [
|
||||||
Text('Pay by MTN Wallet'.tr),
|
// Text('Pay by MTN Wallet'.tr),
|
||||||
const SizedBox(width: 10),
|
// const SizedBox(width: 10),
|
||||||
Image.asset(
|
// Image.asset(
|
||||||
'assets/images/cashMTN.png',
|
// 'assets/images/cashMTN.png',
|
||||||
width: 70,
|
// width: 70,
|
||||||
height: 70,
|
// height: 70,
|
||||||
fit: BoxFit.fill,
|
// fit: BoxFit.fill,
|
||||||
),
|
// ),
|
||||||
],
|
// ],
|
||||||
)),
|
// )),
|
||||||
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Get.back();
|
Get.back();
|
||||||
@@ -151,51 +153,26 @@ class PointsCaptain extends StatelessWidget {
|
|||||||
)),
|
)),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Get.back();
|
// التحقق بالبصمة قبل أي شيء
|
||||||
Get.defaultDialog(
|
|
||||||
barrierDismissible: false,
|
|
||||||
title: 'Insert Wallet phone number'.tr,
|
|
||||||
content: Form(
|
|
||||||
key: paymentController.formKey,
|
|
||||||
child: MyTextForm(
|
|
||||||
controller:
|
|
||||||
paymentController.walletphoneController,
|
|
||||||
label: 'Insert Wallet phone number'.tr,
|
|
||||||
hint: '963941234567',
|
|
||||||
type: TextInputType.phone)),
|
|
||||||
confirm: MyElevatedButton(
|
|
||||||
title: 'OK'.tr,
|
|
||||||
onPressed: () async {
|
|
||||||
Get.back();
|
|
||||||
if (paymentController.formKey.currentState!
|
|
||||||
.validate()) {
|
|
||||||
box.write(
|
|
||||||
BoxName.phoneWallet,
|
|
||||||
paymentController
|
|
||||||
.walletphoneController.text);
|
|
||||||
// await payWithSyriaTelWallet(
|
|
||||||
// context, pricePoint.toString(), 'SYP');
|
|
||||||
bool isAuthSupported =
|
bool isAuthSupported =
|
||||||
await LocalAuthentication()
|
await LocalAuthentication().isDeviceSupported();
|
||||||
.isDeviceSupported();
|
|
||||||
if (isAuthSupported) {
|
if (isAuthSupported) {
|
||||||
bool didAuthenticate =
|
bool didAuthenticate =
|
||||||
await LocalAuthentication()
|
await LocalAuthentication().authenticate(
|
||||||
.authenticate(
|
|
||||||
localizedReason:
|
localizedReason:
|
||||||
'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
|
'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!didAuthenticate) {
|
if (!didAuthenticate) {
|
||||||
if (Get.isDialogOpen ?? false) Get.back();
|
print("❌ User did not authenticate with biometrics");
|
||||||
print(
|
|
||||||
"❌ User did not authenticate with biometrics");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Get.to(() => PaymentScreenSmsProvider(
|
|
||||||
amount: pricePoint));
|
// الانتقال مباشرة لإنشاء الفاتورة بعد النجاح بالبصمة
|
||||||
}
|
Get.to(
|
||||||
}));
|
() => PaymentScreenSmsProvider(amount: pricePoint));
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@@ -210,6 +187,69 @@ class PointsCaptain extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
|
// GestureDetector(
|
||||||
|
// onTap: () async {
|
||||||
|
// Get.back();
|
||||||
|
// Get.defaultDialog(
|
||||||
|
// barrierDismissible: false,
|
||||||
|
// title: 'Insert Wallet phone number'.tr,
|
||||||
|
// content: Form(
|
||||||
|
// key: paymentController.formKey,
|
||||||
|
// child: MyTextForm(
|
||||||
|
// controller:
|
||||||
|
// paymentController.walletphoneController,
|
||||||
|
// label: 'Insert Wallet phone number'.tr,
|
||||||
|
// hint: '963941234567',
|
||||||
|
// type: TextInputType.phone)),
|
||||||
|
// confirm: MyElevatedButton(
|
||||||
|
// title: 'OK'.tr,
|
||||||
|
// onPressed: () async {
|
||||||
|
// Get.back();
|
||||||
|
// if (paymentController.formKey.currentState!
|
||||||
|
// .validate()) {
|
||||||
|
// box.write(
|
||||||
|
// BoxName.phoneWallet,
|
||||||
|
// paymentController
|
||||||
|
// .walletphoneController.text);
|
||||||
|
// // await payWithSyriaTelWallet(
|
||||||
|
// // context, pricePoint.toString(), 'SYP');
|
||||||
|
// bool isAuthSupported =
|
||||||
|
// await LocalAuthentication()
|
||||||
|
// .isDeviceSupported();
|
||||||
|
// if (isAuthSupported) {
|
||||||
|
// bool didAuthenticate =
|
||||||
|
// await LocalAuthentication()
|
||||||
|
// .authenticate(
|
||||||
|
// localizedReason:
|
||||||
|
// 'استخدم بصمة الإصبع أو الوجه لتأكيد الدفع',
|
||||||
|
// );
|
||||||
|
// if (!didAuthenticate) {
|
||||||
|
// if (Get.isDialogOpen ?? false) Get.back();
|
||||||
|
// print(
|
||||||
|
// "❌ User did not authenticate with biometrics");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Get.to(() => PaymentScreenMtn(
|
||||||
|
// amount: pricePoint,
|
||||||
|
// userType: 'Driver',
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
// }));
|
||||||
|
// },
|
||||||
|
// child: Row(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
// children: [
|
||||||
|
// Text('Pay by MTN Wallet'.tr),
|
||||||
|
// const SizedBox(width: 10),
|
||||||
|
// Image.asset(
|
||||||
|
// 'assets/images/cashMTN.png',
|
||||||
|
// width: 70,
|
||||||
|
// height: 70,
|
||||||
|
// fit: BoxFit.fill,
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// )),
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -113,9 +113,11 @@ class WalletCaptainRefactored extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${'Total Points is'.tr} 💎',
|
'رصيد التشغيل 💎',
|
||||||
style: AppStyle.headTitle2
|
style: AppStyle.headTitle2.copyWith(
|
||||||
.copyWith(color: Colors.white, fontWeight: FontWeight.bold),
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -380,17 +382,13 @@ class WalletCaptainRefactored extends StatelessWidget {
|
|||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
children: [
|
children: [
|
||||||
PointsCaptain(
|
PointsCaptain(
|
||||||
kolor: AppColor.greyColor,
|
kolor: AppColor.greyColor, pricePoint: 100, countPoint: '100'),
|
||||||
pricePoint: 10000,
|
|
||||||
countPoint: '10000'),
|
|
||||||
PointsCaptain(
|
PointsCaptain(
|
||||||
kolor: AppColor.bronze, pricePoint: 20000, countPoint: '21000'),
|
kolor: AppColor.bronze, pricePoint: 200, countPoint: '210'),
|
||||||
PointsCaptain(
|
PointsCaptain(
|
||||||
kolor: AppColor.goldenBronze,
|
kolor: AppColor.goldenBronze, pricePoint: 400, countPoint: '450'),
|
||||||
pricePoint: 40000,
|
|
||||||
countPoint: '45000'),
|
|
||||||
PointsCaptain(
|
PointsCaptain(
|
||||||
kolor: AppColor.gold, pricePoint: 100000, countPoint: '110000'),
|
kolor: AppColor.gold, pricePoint: 1000, countPoint: '1100'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ class CarsInsertingPage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(AI());
|
Get.put(AI());
|
||||||
return MyScafolld(
|
return MyScafolld(
|
||||||
title: 'Insert New Car'.tr,
|
title: "Add new car".tr,
|
||||||
body: [
|
body: [
|
||||||
Container(
|
Container(
|
||||||
color: AppColor.accentColor.withOpacity(.2),
|
color: AppColor.accentColor.withOpacity(.2),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(22),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
egyptCarLicenceFront(),
|
syriaCarLicenceFront(),
|
||||||
egyptCarLicenceBack(),
|
syriaCarLicenceBack(),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text('Please make sure to read the license carefully.'.tr),
|
Text('Please make sure to read the license carefully.'.tr),
|
||||||
Text(
|
Text(
|
||||||
@@ -66,7 +66,7 @@ class CarsInsertingPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GetBuilder<AI> egyptCarLicenceFront() {
|
GetBuilder<AI> syriaCarLicenceFront() {
|
||||||
return GetBuilder<AI>(
|
return GetBuilder<AI>(
|
||||||
builder: (ai) {
|
builder: (ai) {
|
||||||
if (ai.responseIdCardDriverEgyptFront.isNotEmpty) {
|
if (ai.responseIdCardDriverEgyptFront.isNotEmpty) {
|
||||||
@@ -162,7 +162,7 @@ GetBuilder<AI> egyptCarLicenceFront() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
GetBuilder<AI> egyptCarLicenceBack() {
|
GetBuilder<AI> syriaCarLicenceBack() {
|
||||||
return GetBuilder<AI>(
|
return GetBuilder<AI>(
|
||||||
builder: (ai) {
|
builder: (ai) {
|
||||||
if (ai.responseIdCardDriverEgyptBack.isNotEmpty) {
|
if (ai.responseIdCardDriverEgyptBack.isNotEmpty) {
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sefer_driver/constant/box_name.dart';
|
||||||
import 'package:sefer_driver/controller/profile/captain_profile_controller.dart';
|
import 'package:sefer_driver/controller/profile/captain_profile_controller.dart';
|
||||||
|
import 'package:sefer_driver/main.dart';
|
||||||
import 'package:sefer_driver/views/auth/captin/criminal_documents_page.dart';
|
import 'package:sefer_driver/views/auth/captin/criminal_documents_page.dart';
|
||||||
import 'package:sefer_driver/views/widgets/my_scafold.dart';
|
import 'package:sefer_driver/views/widgets/my_scafold.dart';
|
||||||
import 'package:sefer_driver/views/widgets/mycircular.dart';
|
import 'package:sefer_driver/views/widgets/mycircular.dart';
|
||||||
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
import 'package:sefer_driver/views/widgets/mydialoug.dart';
|
||||||
|
import '../../../constant/links.dart';
|
||||||
|
import '../../../controller/functions/crud.dart';
|
||||||
import 'behavior_page.dart';
|
import 'behavior_page.dart';
|
||||||
import 'captains_cars.dart';
|
import 'captains_cars.dart';
|
||||||
|
|
||||||
@@ -139,22 +144,24 @@ class ActionsGrid extends StatelessWidget {
|
|||||||
mainAxisSpacing: 16,
|
mainAxisSpacing: 16,
|
||||||
childAspectRatio: 2.5, // للتحكم في ارتفاع الأزرار
|
childAspectRatio: 2.5, // للتحكم في ارتفاع الأزرار
|
||||||
children: [
|
children: [
|
||||||
|
// _ActionTile(
|
||||||
|
// title: 'My Cars'.tr,
|
||||||
|
// icon: Icons.directions_car_filled,
|
||||||
|
// onTap: () => Get.to(() => CaptainsCars()),
|
||||||
|
// ),
|
||||||
|
// _ActionTile(
|
||||||
|
// title: 'Criminal Record'.tr,
|
||||||
|
// icon: Icons.description,
|
||||||
|
// onTap: () => Get.to(() => CriminalDocumemtPage()),
|
||||||
|
// ),
|
||||||
_ActionTile(
|
_ActionTile(
|
||||||
title: 'My Cars'.tr,
|
title: 'ShamCash Account'.tr, // غيرت الاسم ليكون أوضح
|
||||||
icon: Icons.directions_car_filled,
|
icon: Icons.account_balance_wallet_rounded, // أيقونة محفظة
|
||||||
onTap: () => Get.to(() => CaptainsCars()),
|
// trailing: Icon(Icons.arrow_forward_ios,
|
||||||
),
|
// size: 16, color: Colors.grey), // سهم صغير للجمالية
|
||||||
_ActionTile(
|
|
||||||
title: 'Criminal Record'.tr,
|
|
||||||
icon: Icons.description,
|
|
||||||
onTap: () => Get.to(() => CriminalDocumemtPage()),
|
|
||||||
),
|
|
||||||
_ActionTile(
|
|
||||||
title: 'Bank Account'.tr,
|
|
||||||
icon: Icons.account_balance,
|
|
||||||
onTap: () {
|
onTap: () {
|
||||||
MyDialog().getDialog('Coming Soon'.tr,
|
// استدعاء دالة فتح النافذة
|
||||||
'This service will be available soon.'.tr, () => Get.back());
|
showShamCashInput();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_ActionTile(
|
_ActionTile(
|
||||||
@@ -167,6 +174,223 @@ class ActionsGrid extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showShamCashInput() {
|
||||||
|
// 1. القراءة من الذاكرة المحلية (GetStorage) عند فتح النافذة
|
||||||
|
// إذا لم يتم العثور على قيمة، يتم تعيينها إلى نص فارغ
|
||||||
|
final String existingName = box.read('shamcash_name') ?? '';
|
||||||
|
final String existingCode = box.read('shamcash_code') ?? '';
|
||||||
|
|
||||||
|
// تعريف أدوات التحكم للحقلين مع تحميل القيمة المحفوظة
|
||||||
|
final TextEditingController nameController =
|
||||||
|
TextEditingController(text: existingName);
|
||||||
|
final TextEditingController codeController =
|
||||||
|
TextEditingController(text: existingCode);
|
||||||
|
|
||||||
|
Get.bottomSheet(
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(25),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(30)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black26, blurRadius: 10, offset: Offset(0, -2))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// --- 1. المقبض العلوي ---
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
height: 5,
|
||||||
|
width: 50,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[300],
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
margin: const EdgeInsets.only(bottom: 20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- 2. العنوان والأيقونة ---
|
||||||
|
Image.asset(
|
||||||
|
'assets/images/shamCash.png',
|
||||||
|
height: 50,
|
||||||
|
),
|
||||||
|
// const Icon(Icons.account_balance_wallet_rounded,
|
||||||
|
// size: 45, color: Colors.blueAccent),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
"ربط حساب شام كاش 🔗",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.blueGrey[900]),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
const Text(
|
||||||
|
"أدخل بيانات حسابك لاستقبال الأرباح فوراً",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 13, color: Colors.grey),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 25),
|
||||||
|
|
||||||
|
// --- 3. الحقل الأول: اسم الحساب (أعلى الباركود) ---
|
||||||
|
const Text("1. اسم الحساب (أعلى الباركود)",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[50],
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey[300]!),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: nameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "مثال: intaleq",
|
||||||
|
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 13),
|
||||||
|
border: InputBorder.none,
|
||||||
|
prefixIcon: const Icon(Icons.person_outline_rounded,
|
||||||
|
color: Colors.blueGrey),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 15, horizontal: 10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
|
||||||
|
// --- 4. الحقل الثاني: الكود (أسفل الباركود) ---
|
||||||
|
const Text("2. كود المحفظة (أسفل الباركود)",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[50],
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey[300]!),
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: codeController,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
letterSpacing: 0.5), // خط أصغر قليلاً للكود الطويل
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "مثال: 80f23afe40...",
|
||||||
|
hintStyle: TextStyle(color: Colors.grey[400], fontSize: 13),
|
||||||
|
border: InputBorder.none,
|
||||||
|
prefixIcon: const Icon(Icons.qr_code_2_rounded,
|
||||||
|
color: Colors.blueGrey),
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 15, horizontal: 10),
|
||||||
|
|
||||||
|
// زر لصق الكود
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: const Icon(Icons.paste_rounded, color: Colors.blue),
|
||||||
|
tooltip: "لصق الكود",
|
||||||
|
onPressed: () async {
|
||||||
|
ClipboardData? data =
|
||||||
|
await Clipboard.getData(Clipboard.kTextPlain);
|
||||||
|
if (data != null && data.text != null) {
|
||||||
|
codeController.text = data.text!;
|
||||||
|
// تحريك المؤشر للنهاية بعد اللصق
|
||||||
|
codeController.selection = TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: codeController.text.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
|
||||||
|
// --- 5. زر الحفظ ---
|
||||||
|
SizedBox(
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
String name = nameController.text.trim();
|
||||||
|
String code = codeController.text.trim();
|
||||||
|
|
||||||
|
// التحقق من صحة البيانات
|
||||||
|
if (name.isNotEmpty && code.length > 5) {
|
||||||
|
// 1. إرسال البيانات إلى السيرفر
|
||||||
|
var res = await CRUD()
|
||||||
|
.post(link: AppLink.updateShamCashDriver, payload: {
|
||||||
|
"id": box.read(BoxName.driverID),
|
||||||
|
"accountBank": name,
|
||||||
|
"bankCode": code,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res != 'failure') {
|
||||||
|
// 2. 🔴 الحفظ في الذاكرة المحلية (GetStorage) بعد نجاح التحديث
|
||||||
|
box.write('shamcash_name', name);
|
||||||
|
box.write('shamcash_code', code);
|
||||||
|
|
||||||
|
Get.back(); // إغلاق النافذة
|
||||||
|
Get.snackbar(
|
||||||
|
"تم الحفظ بنجاح",
|
||||||
|
"تم ربط حساب ($name) لاستلام الأرباح.",
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
margin: const EdgeInsets.all(20),
|
||||||
|
icon: const Icon(Icons.check_circle_outline,
|
||||||
|
color: Colors.white),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// في حال فشل الإرسال إلى السيرفر
|
||||||
|
Get.snackbar(
|
||||||
|
"خطأ في السيرفر",
|
||||||
|
"فشل تحديث البيانات، يرجى المحاولة لاحقاً.",
|
||||||
|
backgroundColor: Colors.redAccent,
|
||||||
|
colorText: Colors.white,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
margin: const EdgeInsets.all(20),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
"بيانات ناقصة",
|
||||||
|
"يرجى التأكد من إدخال الاسم والكود بشكل صحيح.",
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
colorText: Colors.white,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
margin: const EdgeInsets.all(20),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF2ecc71), // الأخضر المالي
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
|
elevation: 2,
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
"حفظ وتفعيل الحساب",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10), // مسافة سفلية إضافية للأمان
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isScrollControlled: true, // ضروري لرفع النافذة عند فتح الكيبورد
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// ويدجت داخلية لزر في الشبكة
|
/// ويدجت داخلية لزر في الشبكة
|
||||||
class _ActionTile extends StatelessWidget {
|
class _ActionTile extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:sefer_driver/constant/colors.dart';
|
|
||||||
import 'package:sefer_driver/constant/style.dart';
|
|
||||||
import 'package:sefer_driver/controller/notification/ride_available_controller.dart';
|
|
||||||
import 'package:sefer_driver/views/widgets/my_scafold.dart';
|
|
||||||
import 'package:sefer_driver/views/widgets/mycircular.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:google_maps_flutter/google_maps_flutter.dart'; // لتحديد الأنواع إذا لزم
|
||||||
|
|
||||||
import '../../constant/box_name.dart';
|
import '../../constant/box_name.dart';
|
||||||
|
import '../../constant/colors.dart';
|
||||||
import '../../constant/links.dart';
|
import '../../constant/links.dart';
|
||||||
import '../../controller/firebase/firbase_messge.dart';
|
import '../../constant/style.dart';
|
||||||
import '../../controller/functions/crud.dart';
|
import '../../controller/functions/crud.dart';
|
||||||
import '../../controller/home/captin/home_captain_controller.dart';
|
import '../../controller/home/captin/home_captain_controller.dart';
|
||||||
import '../../main.dart';
|
import '../../controller/notification/ride_available_controller.dart';
|
||||||
|
import '../../main.dart'; // للوصول للـ box
|
||||||
import '../home/Captin/driver_map_page.dart';
|
import '../home/Captin/driver_map_page.dart';
|
||||||
|
import '../widgets/my_scafold.dart';
|
||||||
|
import '../widgets/mycircular.dart';
|
||||||
import '../widgets/mydialoug.dart';
|
import '../widgets/mydialoug.dart';
|
||||||
|
|
||||||
class AvailableRidesPage extends StatelessWidget {
|
class AvailableRidesPage extends StatelessWidget {
|
||||||
@@ -22,124 +22,87 @@ class AvailableRidesPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Get.put(RideAvailableController());
|
// حقن الكنترولر (تأكد أنك تستخدم الكود الجديد الذي أعطيتك إياه للكنترولر)
|
||||||
|
Get.lazyPut(() => RideAvailableController());
|
||||||
|
Get.lazyPut(() => HomeCaptainController());
|
||||||
|
|
||||||
return GetBuilder<RideAvailableController>(
|
return GetBuilder<RideAvailableController>(
|
||||||
builder: (rideAvailableController) {
|
builder: (controller) {
|
||||||
// rideAvailableController.sortRidesByDistance();
|
|
||||||
return MyScafolld(
|
return MyScafolld(
|
||||||
title: 'Available for rides'.tr,
|
title: 'Available for rides'.tr,
|
||||||
|
isleading: true,
|
||||||
body: [
|
body: [
|
||||||
rideAvailableController.isLoading
|
controller.isLoading
|
||||||
? const MyCircularProgressIndicator()
|
? const Center(
|
||||||
:
|
child: Padding(
|
||||||
// : ListView.builder(
|
padding: EdgeInsets.only(top: 50.0),
|
||||||
// itemCount: rideAvailableController
|
child: MyCircularProgressIndicator(),
|
||||||
// .rideAvailableMap['message']
|
))
|
||||||
// .where((rideInfo) {
|
: Builder(
|
||||||
// var driverType =
|
builder: (context) {
|
||||||
// box.read(BoxName.carTypeOfDriver).toString();
|
// 1. الفلترة حسب نوع السيارة (تم نقل المنطق للكنترولر، لكن هنا للعرض فقط)
|
||||||
// return (driverType == 'Comfort' &&
|
// الكنترولر الجديد يفلتر عند الإضافة، لكن لا ضرر من التأكيد هنا
|
||||||
// ['Speed', 'Comfort']
|
final ridesList = controller.availableRides;
|
||||||
// .contains(rideInfo['carType'])) ||
|
|
||||||
// (driverType == 'Speed' &&
|
|
||||||
// rideInfo['carType'] == 'Speed') ||
|
|
||||||
// (driverType == 'Scooter' &&
|
|
||||||
// rideInfo['carType'] == 'Scooter') ||
|
|
||||||
// (driverType == 'Awfar Car' &&
|
|
||||||
// rideInfo['carType'] == 'Awfar Car') ||
|
|
||||||
// (driverType == 'Lady' &&
|
|
||||||
// ['Comfort', 'Speed', 'Lady']
|
|
||||||
// .contains(rideInfo['carType']));
|
|
||||||
// }).length,
|
|
||||||
// itemBuilder: (context, index) {
|
|
||||||
// var filteredRides = rideAvailableController
|
|
||||||
// .rideAvailableMap['message']
|
|
||||||
// .where((rideInfo) {
|
|
||||||
// var driverType =
|
|
||||||
// box.read(BoxName.carTypeOfDriver).toString();
|
|
||||||
// return (driverType == 'Comfort' &&
|
|
||||||
// ['Speed', 'Comfort']
|
|
||||||
// .contains(rideInfo['carType'])) ||
|
|
||||||
// (driverType == 'Speed' &&
|
|
||||||
// rideInfo['carType'] == 'Speed') ||
|
|
||||||
// (driverType == 'Awfar Car' &&
|
|
||||||
// rideInfo['carType'] == 'Awfar Car') ||
|
|
||||||
// (driverType == 'Scooter' &&
|
|
||||||
// rideInfo['carType'] == 'Scooter') ||
|
|
||||||
// (driverType == 'Lady' &&
|
|
||||||
// ['Comfort', 'Speed', 'Lady']
|
|
||||||
// .contains(rideInfo['carType']));
|
|
||||||
// }).toList();
|
|
||||||
|
|
||||||
// return RideAvailableCard(
|
if (ridesList.isEmpty) {
|
||||||
// rideInfo: filteredRides[index],
|
return Center(
|
||||||
// );
|
child: Column(
|
||||||
// },
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
// )
|
children: [
|
||||||
ListView.builder(
|
const SizedBox(height: 100),
|
||||||
itemCount: rideAvailableController
|
Icon(CupertinoIcons.car_detailed,
|
||||||
.rideAvailableMap['message']
|
size: 60,
|
||||||
.where((rideInfo) {
|
color:
|
||||||
var driverType =
|
AppColor.primaryColor.withOpacity(0.5)),
|
||||||
box.read(BoxName.carTypeOfDriver).toString();
|
const SizedBox(height: 20),
|
||||||
switch (driverType) {
|
Text(
|
||||||
case 'Comfort':
|
"No rides available right now.".tr,
|
||||||
return ['Speed', 'Comfort']
|
style: AppStyle.subtitle,
|
||||||
.contains(rideInfo['carType']);
|
),
|
||||||
case 'Speed':
|
const SizedBox(height: 20),
|
||||||
case 'Scooter':
|
ElevatedButton.icon(
|
||||||
case 'Awfar Car':
|
onPressed: () => controller.getRideAvailable(
|
||||||
return rideInfo['carType'] == driverType;
|
forceRefresh: true),
|
||||||
case 'Lady':
|
icon: const Icon(Icons.refresh),
|
||||||
return ['Comfort', 'Speed', 'Lady']
|
label: Text("Refresh Market".tr),
|
||||||
.contains(rideInfo['carType']);
|
style: ElevatedButton.styleFrom(
|
||||||
default:
|
backgroundColor: AppColor.primaryColor,
|
||||||
return false;
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}).length,
|
|
||||||
|
// 2. عرض القائمة
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
await controller.getRideAvailable(forceRefresh: true);
|
||||||
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12, vertical: 16),
|
||||||
|
itemCount: ridesList.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
var filteredRides = rideAvailableController
|
|
||||||
.rideAvailableMap['message']
|
|
||||||
.where((rideInfo) {
|
|
||||||
var driverType =
|
|
||||||
box.read(BoxName.carTypeOfDriver).toString();
|
|
||||||
switch (driverType) {
|
|
||||||
case 'Comfort':
|
|
||||||
return ['Speed', 'Comfort']
|
|
||||||
.contains(rideInfo['carType']);
|
|
||||||
case 'Speed':
|
|
||||||
case 'Scooter':
|
|
||||||
case 'Awfar Car':
|
|
||||||
return rideInfo['carType'] == driverType;
|
|
||||||
case 'Lady':
|
|
||||||
return ['Comfort', 'Speed', 'Lady']
|
|
||||||
.contains(rideInfo['carType']);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
return RideAvailableCard(
|
return RideAvailableCard(
|
||||||
rideInfo: filteredRides[index],
|
rideInfo: ridesList[index],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
// rideAvailableController.isLoading
|
|
||||||
// ? const MyCircularProgressIndicator()
|
|
||||||
// : ListView.builder(
|
|
||||||
// itemCount: rideAvailableController
|
|
||||||
// .rideAvailableMap['message'].length,
|
|
||||||
// itemBuilder: (context, index) => RideAvailableCard(
|
|
||||||
// rideInfo: rideAvailableController
|
|
||||||
// .rideAvailableMap['message'][index],
|
|
||||||
// ),
|
|
||||||
// )
|
|
||||||
],
|
],
|
||||||
isleading: true);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// بطاقة الرحلة (The Card)
|
||||||
|
// =============================================================================
|
||||||
class RideAvailableCard extends StatelessWidget {
|
class RideAvailableCard extends StatelessWidget {
|
||||||
final Map<String, dynamic> rideInfo;
|
final Map<String, dynamic> rideInfo;
|
||||||
|
|
||||||
@@ -148,206 +111,259 @@ class RideAvailableCard extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.all(8.0),
|
margin: const EdgeInsets.only(bottom: 16.0),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
|
shadowColor: Colors.black.withOpacity(0.1),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildLocationRow('↑', rideInfo['startName'], AppColor.greenColor),
|
_buildHeader(),
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildLocationRow('↓', rideInfo['endName'], Colors.red),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildInfoRow(),
|
_buildRouteInfo(),
|
||||||
const SizedBox(height: 16),
|
const Divider(height: 24, thickness: 0.5),
|
||||||
_buildActionRow(),
|
_buildRideDetails(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildAcceptButton(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLocationRow(String icon, String location, Color iconColor) {
|
// ---------------------------------------------------------------------------
|
||||||
return Row(
|
// تصميم البطاقة (Header, Route, Details)
|
||||||
children: [
|
// ---------------------------------------------------------------------------
|
||||||
Text(
|
Widget _buildHeader() {
|
||||||
icon,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20, fontWeight: FontWeight.bold, color: iconColor),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
location,
|
|
||||||
style: AppStyle.subtitle,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildInfoRow() {
|
|
||||||
return Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text('${'Price:'.tr} ${rideInfo['price']} \$', style: AppStyle.title),
|
|
||||||
Text(
|
|
||||||
rideInfo['carType'],
|
|
||||||
style: AppStyle.title.copyWith(color: AppColor.greenColor),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildActionRow() {
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('📈 ${rideInfo['passengerRate']}', style: AppStyle.title),
|
Text('Price'.tr, style: AppStyle.subtitle.copyWith(fontSize: 12)),
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
Text(
|
||||||
'📍 ${rideInfo['distance']} ${'KM'.tr}',
|
'${rideInfo['price']} ${'SYP'.tr}', // العملة
|
||||||
style: AppStyle.title.copyWith(color: AppColor.greenColor),
|
style: AppStyle.title.copyWith(
|
||||||
|
fontSize: 20, color: AppColor.primaryColor, height: 1.2),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
Container(
|
||||||
onPressed: () => _acceptRide(),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
style: ElevatedButton.styleFrom(
|
decoration: BoxDecoration(
|
||||||
backgroundColor: AppColor.greenColor,
|
color: AppColor.greenColor.withOpacity(0.1),
|
||||||
shape:
|
borderRadius: BorderRadius.circular(20),
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
border: Border.all(color: AppColor.greenColor.withOpacity(0.3)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
rideInfo['carType'] ?? 'Fixed Price'.tr,
|
||||||
|
style: AppStyle.title
|
||||||
|
.copyWith(color: AppColor.greenColor, fontSize: 13),
|
||||||
),
|
),
|
||||||
child: Text('Accept'.tr),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _acceptRide() async {
|
Widget _buildRouteInfo() {
|
||||||
var res = await CRUD().post(link: AppLink.updateStausFromSpeed, payload: {
|
return Row(
|
||||||
'id': rideInfo['id'],
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
'rideTimeStart': DateTime.now().toString(),
|
children: [
|
||||||
'status': 'Apply',
|
Column(
|
||||||
'driver_id': box.read(BoxName.driverID),
|
children: [
|
||||||
});
|
const Icon(Icons.my_location,
|
||||||
if (AppLink.endPoint.toString() != AppLink.seferCairoServer) {
|
color: AppColor.primaryColor, size: 18),
|
||||||
CRUD().post(
|
Container(
|
||||||
link: '${AppLink.endPoint}rides/updateStausFromSpeed.php',
|
height: 30,
|
||||||
payload: {
|
width: 1,
|
||||||
'id': rideInfo['id'],
|
color: Colors.grey.shade300,
|
||||||
'rideTimeStart': DateTime.now().toString(),
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||||
'status': 'Apply',
|
),
|
||||||
'driver_id': box.read(BoxName.driverID),
|
const Icon(Icons.location_on, color: Colors.red, size: 18),
|
||||||
});
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(rideInfo['startName'] ?? 'Unknown Location'.tr,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: AppStyle.title.copyWith(fontSize: 14)),
|
||||||
|
const SizedBox(height: 22),
|
||||||
|
Text(rideInfo['endName'] ?? 'Destination'.tr,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: AppStyle.title.copyWith(fontSize: 14)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// .then((value) {
|
Widget _buildRideDetails() {
|
||||||
// var json = jsonDecode(res);
|
return Row(
|
||||||
if (res != "failure") {
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
List<String> bodyToPassenger = [
|
children: [
|
||||||
box.read(BoxName.driverID).toString(),
|
_infoItem(Icons.social_distance, '${rideInfo['distance']} KM'),
|
||||||
box.read(BoxName.nameDriver).toString(),
|
_infoItem(Icons.access_time, '${rideInfo['duration']} Min'),
|
||||||
box.read(BoxName.tokenDriver).toString(),
|
_infoItem(Icons.star, '${rideInfo['passengerRate'] ?? 5.0}',
|
||||||
];
|
iconColor: Colors.amber),
|
||||||
box.write(BoxName.statusDriverLocation, 'on');
|
],
|
||||||
await CRUD().postFromDialogue(link: AppLink.addDriverOrder, payload: {
|
);
|
||||||
'driver_id': box.read(BoxName.driverID),
|
|
||||||
'order_id': rideInfo['id'],
|
|
||||||
'status': 'Apply'
|
|
||||||
});
|
|
||||||
await CRUD().post(link: AppLink.updateRides, payload: {
|
|
||||||
'id': rideInfo['id'],
|
|
||||||
'DriverIsGoingToPassenger': DateTime.now().toString(),
|
|
||||||
'status': 'Applied'
|
|
||||||
});
|
|
||||||
await CRUD().post(
|
|
||||||
link: AppLink.updateWaitingRide,
|
|
||||||
payload: {'id': rideInfo['id'], 'status': 'Applied'});
|
|
||||||
if (AppLink.endPoint.toString() != AppLink.seferCairoServer) {
|
|
||||||
CRUD().postFromDialogue(
|
|
||||||
link: '${AppLink.endPoint}/driver_order/add.php',
|
|
||||||
payload: {
|
|
||||||
'driver_id': box.read(BoxName.driverID),
|
|
||||||
// box.read(BoxName.driverID).toString(),
|
|
||||||
'order_id': rideInfo['id'],
|
|
||||||
'status': 'Apply'
|
|
||||||
});
|
|
||||||
CRUD().post(link: '${AppLink.endPoint}/rides/update.php', payload: {
|
|
||||||
'id': rideInfo['id'],
|
|
||||||
'DriverIsGoingToPassenger': DateTime.now().toString(),
|
|
||||||
'status': 'Applied'
|
|
||||||
});
|
|
||||||
CRUD().post(
|
|
||||||
link:
|
|
||||||
"${AppLink.endPoint}/ride/notificationCaptain/updateWaitingTrip.php",
|
|
||||||
payload: {'id': rideInfo['id'], 'status': 'Applied'});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FirebaseMessagesController().sendNotificationToPassengerToken(
|
Widget _infoItem(IconData icon, String text,
|
||||||
"Accepted Ride".tr,
|
{Color iconColor = Colors.grey}) {
|
||||||
'your ride is Accepted'.tr,
|
return Row(
|
||||||
// arguments['DriverList'][9].toString(),
|
children: [
|
||||||
rideInfo['passengerToken'].toString(),
|
Icon(icon, size: 16, color: iconColor),
|
||||||
// box.read(BoxName.tokenDriver).toString(),
|
const SizedBox(width: 4),
|
||||||
bodyToPassenger,
|
Text(text, style: AppStyle.subtitle.copyWith(fontSize: 13)),
|
||||||
'start.wav');
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// زر القبول والمنطق الكامل (Accept Logic) 🔥
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
Widget _buildAcceptButton() {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _acceptRideNewLogic,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColor.greenColor,
|
||||||
|
shape:
|
||||||
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
elevation: 2,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Accept Ride'.tr,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔥🔥🔥 الوظيفة الأهم: قبول الرحلة وتجهيز البيانات 🔥🔥🔥
|
||||||
|
void _acceptRideNewLogic() async {
|
||||||
|
// 1. إظهار Loading
|
||||||
|
Get.dialog(
|
||||||
|
const Center(child: MyCircularProgressIndicator()),
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String driverId = box.read(BoxName.driverID).toString();
|
||||||
|
|
||||||
|
// 2. إرسال الطلب للسيرفر (acceptRide.php الجديد)
|
||||||
|
var response = await CRUD().post(
|
||||||
|
link: "${AppLink.ride}/rides/acceptRide.php",
|
||||||
|
payload: {
|
||||||
|
'id': rideInfo['id'].toString(),
|
||||||
|
'driver_id': driverId,
|
||||||
|
'status': 'Apply', // الحالة المتفق عليها
|
||||||
|
'passengerToken': rideInfo['passengerToken'].toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// إخفاء الـ Loading
|
||||||
Get.back();
|
Get.back();
|
||||||
Get.to(() => PassengerLocationMapPage(), arguments: {
|
|
||||||
|
// 3. تحليل الرد
|
||||||
|
var jsonResponse = jsonDecode(response);
|
||||||
|
|
||||||
|
if (jsonResponse['status'] == 'success') {
|
||||||
|
// ✅ نجاح: أنت الفائز بالرحلة
|
||||||
|
|
||||||
|
// تحديث حالة السائق محلياً
|
||||||
|
Get.find<HomeCaptainController>().changeRideId();
|
||||||
|
box.write(BoxName.statusDriverLocation, 'on');
|
||||||
|
|
||||||
|
// 🔥 تجهيز الـ Arguments كاملة (Mapping) 📦
|
||||||
|
// نأخذ البيانات من rideInfo (القادمة من getRideWaiting) ونمررها للخريطة
|
||||||
|
Map<String, dynamic> fullRideArgs = {
|
||||||
|
// معرفات الرحلة
|
||||||
|
'rideId': rideInfo['id'].toString(),
|
||||||
|
'passengerId': rideInfo['passengerId'].toString(),
|
||||||
|
'driverId': driverId,
|
||||||
|
|
||||||
|
// المواقع (يجب أن تكون Strings بصيغة "lat,lng")
|
||||||
'passengerLocation': rideInfo['start_location'].toString(),
|
'passengerLocation': rideInfo['start_location'].toString(),
|
||||||
'passengerDestination': rideInfo['end_location'].toString(),
|
'passengerDestination': rideInfo['end_location'].toString(),
|
||||||
'Duration': rideInfo['duration'].toString(),
|
|
||||||
'totalCost': rideInfo['price'].toString(),
|
// الأسماء والعناوين
|
||||||
'Distance': rideInfo['distance'].toString(),
|
'startNameLocation': rideInfo['startName'].toString(),
|
||||||
'name': rideInfo['first_name'].toString(),
|
'endNameLocation': rideInfo['endName'].toString(),
|
||||||
|
|
||||||
|
// تفاصيل الراكب
|
||||||
|
'name': rideInfo['first_name'] ?? 'Passenger',
|
||||||
'phone': rideInfo['phone'].toString(),
|
'phone': rideInfo['phone'].toString(),
|
||||||
'email': rideInfo['email'].toString(),
|
'email': rideInfo['email'] ?? '',
|
||||||
'WalletChecked': rideInfo['payment_method'].toString(),
|
|
||||||
'tokenPassenger': rideInfo['passengerToken'].toString(),
|
'tokenPassenger': rideInfo['passengerToken'].toString(),
|
||||||
'direction':
|
'passengerWalletBurc': rideInfo['bruc'].toString(), // رصيد الراكب
|
||||||
'https://www.google.com/maps/dir/${rideInfo['start_location']}/${rideInfo['end_location']}/',
|
|
||||||
'DurationToPassenger': rideInfo['duration'].toString(),
|
// التفاصيل المالية والرحلة
|
||||||
'rideId': rideInfo['id'].toString(),
|
'totalCost': rideInfo['price'].toString(), // السعر الكلي
|
||||||
'passengerId': rideInfo['passenger_id'].toString(),
|
'paymentAmount': rideInfo['price'].toString(), // المبلغ المطلوب
|
||||||
'driverId': box.read(BoxName.driverID).toString(),
|
'Distance': rideInfo['distance'].toString(),
|
||||||
'durationOfRideValue': rideInfo['duration'].toString(),
|
'Duration': rideInfo['duration'].toString(),
|
||||||
'paymentAmount': rideInfo['price'].toString(),
|
'durationOfRideValue':
|
||||||
'paymentMethod': 'cash'.toString() == //todo fix payment method
|
rideInfo['duration'].toString(), // تكرار للتأكد
|
||||||
'true'
|
'carType': rideInfo['carType'].toString(),
|
||||||
|
|
||||||
|
// الدفع والمحفظة
|
||||||
|
'paymentMethod': (rideInfo['payment_method'] == 'visa' ||
|
||||||
|
rideInfo['payment_method'] == 'wallet')
|
||||||
? 'visa'
|
? 'visa'
|
||||||
: 'cash',
|
: 'cash',
|
||||||
'isHaveSteps': 'startEnd'.toString(),
|
'WalletChecked':
|
||||||
'step0': ''.toString(),
|
rideInfo['passenger_wallet'].toString() != '0' ? 'true' : 'false',
|
||||||
'step1': ''.toString(),
|
'kazan': Get.find<HomeCaptainController>()
|
||||||
'step2': ''.toString(),
|
.kazan
|
||||||
'step3': ''.toString(),
|
.toString(), // نسبة الشركة (من الكنترولر)
|
||||||
'step4': ''.toString(),
|
|
||||||
'passengerWalletBurc': rideInfo['bruc'].toString(),
|
// بيانات إضافية (لتجنب الـ Null Safety errors)
|
||||||
|
'direction':
|
||||||
|
'http://googleusercontent.com/maps.google.com/maps?saddr=${rideInfo['start_location']}&daddr=${rideInfo['end_location']}',
|
||||||
'timeOfOrder': DateTime.now().toString(),
|
'timeOfOrder': DateTime.now().toString(),
|
||||||
'totalPassenger': rideInfo['price'].toString(),
|
'isHaveSteps': 'false', // لو كان عندك خطوات في الـ waitingRides ضيفها
|
||||||
'carType': rideInfo['carType'].toString(),
|
'step0': '', 'step1': '', 'step2': '', 'step3': '', 'step4': '',
|
||||||
'kazan': Get.find<HomeCaptainController>().kazan.toString(),
|
};
|
||||||
});
|
|
||||||
|
// حفظ البيانات في الصندوق احتياطياً (Crash Recovery)
|
||||||
|
box.write(BoxName.rideArguments, fullRideArgs);
|
||||||
|
|
||||||
|
// الانتقال لصفحة الخريطة ومسح الصفحات السابقة لضمان عدم الرجوع للسوق
|
||||||
|
Get.offAll(() => PassengerLocationMapPage(), arguments: fullRideArgs);
|
||||||
} else {
|
} else {
|
||||||
|
// ❌ فشل: الرحلة أخذها سائق آخر
|
||||||
|
// نقوم بتحديث القائمة فوراً
|
||||||
|
Get.find<RideAvailableController>()
|
||||||
|
.getRideAvailable(forceRefresh: true);
|
||||||
|
|
||||||
MyDialog().getDialog(
|
MyDialog().getDialog(
|
||||||
"This ride is already taken by another driver.".tr, '', () {
|
"Trip taken".tr,
|
||||||
CRUD().post(
|
"This ride was just accepted by another driver.".tr,
|
||||||
link: AppLink.deleteAvailableRide, payload: {'id': rideInfo['id']});
|
() => Get.back(), // زر الموافقة
|
||||||
if (AppLink.endPoint.toString() != AppLink.seferCairoServer) {
|
);
|
||||||
CRUD().post(
|
|
||||||
link:
|
|
||||||
'${AppLink.endPoint}/ride/notificationCaptain/deleteAvailableRide.php',
|
|
||||||
payload: {'id': rideInfo['id']});
|
|
||||||
}
|
}
|
||||||
Get.back();
|
} catch (e) {
|
||||||
});
|
Get.back(); // إخفاء اللودينج في حال الخطأ
|
||||||
|
print("Accept Ride Error: $e");
|
||||||
|
MyDialog().getDialog(
|
||||||
|
"Error".tr,
|
||||||
|
"An unexpected error occurred. Please try again.".tr,
|
||||||
|
() => Get.back(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class MyDialog extends GetxController {
|
|||||||
title: Column(
|
title: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title.tr,
|
||||||
style: AppStyle.title.copyWith(
|
style: AppStyle.title.copyWith(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
|
#include <objectbox_flutter_libs/objectbox_flutter_libs_plugin.h>
|
||||||
#include <record_linux/record_linux_plugin.h>
|
#include <record_linux/record_linux_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
@@ -18,6 +19,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) objectbox_flutter_libs_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "ObjectboxFlutterLibsPlugin");
|
||||||
|
objectbox_flutter_libs_plugin_register_with_registrar(objectbox_flutter_libs_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) record_linux_registrar =
|
g_autoptr(FlPluginRegistrar) record_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin");
|
||||||
record_linux_plugin_register_with_registrar(record_linux_registrar);
|
record_linux_plugin_register_with_registrar(record_linux_registrar);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
flutter_secure_storage_linux
|
flutter_secure_storage_linux
|
||||||
|
objectbox_flutter_libs
|
||||||
record_linux
|
record_linux
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import google_sign_in_ios
|
|||||||
import just_audio
|
import just_audio
|
||||||
import local_auth_darwin
|
import local_auth_darwin
|
||||||
import location
|
import location
|
||||||
|
import objectbox_flutter_libs
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import record_darwin
|
import record_darwin
|
||||||
@@ -55,6 +56,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
||||||
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
|
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
|
||||||
LocationPlugin.register(with: registry.registrar(forPlugin: "LocationPlugin"))
|
LocationPlugin.register(with: registry.registrar(forPlugin: "LocationPlugin"))
|
||||||
|
ObjectboxFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "ObjectboxFlutterLibsPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin"))
|
RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin"))
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 361 B After Width: | Height: | Size: 367 B |