Update: 2026-07-11 05:39:23

This commit is contained in:
Hamza-Ayed
2026-07-11 05:39:23 +03:00
parent 8b19adeb80
commit 06dc0aaffe
31 changed files with 845 additions and 3846 deletions
-1
View File
@@ -116,7 +116,6 @@ flutter {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
implementation 'com.scottyab:rootbeer-lib:0.1.0'
implementation 'com.google.android.gms:play-services-safetynet:18.1.0'
implementation 'com.google.android.gms:play-services-location:21.3.0'
}
@@ -7,22 +7,27 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.Icon
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall
/**
* Android 16 Progress Style Live Update Plugin
* خدمة تحديث حالة الرحلة في شريط الإشعارات على أندرويد.
*
* يستخدم NotificationCompat مع setProgressStyle لتوفير
* تجربة Live Update متقدمة على Android 16 (API 36) والأجهزة الأحدث.
*
* الميزات:
* - شريط تقدم متحرك (Progress Style)
* - تحديث مباشر للوقت المتبقي
* - دعم الحالات المختلفة (بحث، في الطريق، وصل، رحلة جارية)
* - إشعار ثابت (Ongoing) لا يمكن إلغاؤه أثناء الرحلة
* تعمل على ثلاثة مستويات حسب الجهاز:
* - أندرويد 16 فما فوق (API 36+) مع موافقة المستخدم: تُستخدم
* ميزة Live Updates الحقيقية عبر Notification.ProgressStyle
* (نفس الفئة والدوال التي تحققنا منها مباشرة من ملف الـSDK).
* - أندرويد 16 فما فوق بدون موافقة المستخدم، أو أي إصدار من
* API 23 حتى 35: إشعار عادٍ ثابت (Ongoing) بشريط تقدّم كلاسيكي،
* يعمل بشكل متطابق تقريباً بصرياً لكن بدون معاملة النظام الخاصة.
* - أقل من API 26: لا يتم إنشاء قناة إشعارات (الكلاس غير موجود
* أصلاً في النظام قبل هذا الإصدار)، ويُستخدم مسار الإشعار
* الكلاسيكي مباشرة بدون قناة.
*/
class AndroidLiveUpdatePlugin(private val context: Context) {
@@ -31,9 +36,7 @@ class AndroidLiveUpdatePlugin(private val context: Context) {
private const val CHANNEL_NAME = "Siro Live Ride Progress"
private const val CHANNEL_DESC = "Live progress updates for your current Siro ride"
private const val NOTIFICATION_ID = 999
private const val SUMMARY_NOTIFICATION_ID = 1000
// Ride status constants
private const val STATUS_SEARCHING = "searching"
private const val STATUS_DRIVER_ON_WAY = "driver_on_way"
private const val STATUS_DRIVER_ARRIVED = "driver_arrived"
@@ -51,34 +54,66 @@ class AndroidLiveUpdatePlugin(private val context: Context) {
private var currentBody: String = ""
init {
createNotificationChannel()
// NotificationChannel نفسها غير موجودة في النظام قبل API 26،
// فاستدعاؤها بدون هذا الشرط يتسبب بانهيار فوري للتطبيق عند
// الإقلاع على أي جهاز أقدم (Android 6/7).
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
}
}
/**
* إنشاء قناة الإشعارات المخصصة للرحلة
* مع دعم Android 16 Progress Style
*/
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW // Low importance for progress style
NotificationManager.IMPORTANCE_LOW
).apply {
description = CHANNEL_DESC
setShowBadge(false)
enableVibration(false)
enableLights(false)
// Android 16: دعم التقدم المستمر
if (Build.VERSION.SDK_INT >= 36) {
// يمكن إضافة خصائص Android 16 هنا
}
}
notificationManager.createNotificationChannel(channel)
}
/**
* هل يمكن فعلياً استخدام Live Updates الحقيقية الآن؟
* تتطلب أندرويد 16+ وموافقة صريحة من المستخدم عبر إعدادات النظام
* (Settings > Apps > Siro > Notifications > Live Updates).
*/
@Suppress("NewApi")
private fun canUsePromotedLiveUpdate(): Boolean {
if (Build.VERSION.SDK_INT < 36) return false
return try {
notificationManager.canPostPromotedNotifications()
} catch (e: Exception) {
false
}
}
private fun statusColor(): Int = when (currentStatus) {
STATUS_DRIVER_ARRIVED -> Color.parseColor("#22C55E")
STATUS_IN_PROGRESS -> Color.parseColor("#1A2340")
STATUS_SEARCHING -> Color.parseColor("#F59E0B")
else -> Color.parseColor("#1A2340")
}
private fun buildContentIntent(): PendingIntent {
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra("notification_ride", true)
}
return PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
/**
* بدء التحديث المباشر للرحلة
* يعرض إشعاراً ثابتاً مع شريط تقدم
*/
fun startLiveUpdate(
rideId: String,
@@ -93,19 +128,11 @@ class AndroidLiveUpdatePlugin(private val context: Context) {
currentProgress = (progress * 100).toInt().coerceIn(0, 100)
currentMaxProgress = 100
showNotification(
title = currentTitle,
body = currentBody,
progress = currentProgress,
maxProgress = currentMaxProgress,
indeterminate = false,
ongoing = true
)
render(indeterminate = false)
}
/**
* تحديث التقدم والوقت المتبقي
* يستخدم Progress Style مع animate للتحديثات
*/
fun updateProgress(progress: Double, etaText: String) {
currentProgress = (progress * 100).toInt().coerceIn(0, 100)
@@ -122,19 +149,11 @@ class AndroidLiveUpdatePlugin(private val context: Context) {
else -> currentBody
}
showNotification(
title = currentTitle,
body = currentBody,
progress = currentProgress,
maxProgress = currentMaxProgress,
indeterminate = false,
ongoing = true
)
render(indeterminate = false)
}
/**
* تحديث حالة الرحلة بالكامل
* يستخدم لتغيير الحالة بين (في الطريق، وصل، رحلة جارية)
*/
fun updateStatus(
status: String,
@@ -150,50 +169,23 @@ class AndroidLiveUpdatePlugin(private val context: Context) {
STATUS_SEARCHING -> {
currentTitle = "🔍 جاري البحث عن سائق"
currentBody = etaText
showNotification(
title = currentTitle,
body = currentBody,
progress = 0,
maxProgress = 0,
indeterminate = true,
ongoing = true
)
render(indeterminate = true)
}
STATUS_DRIVER_ON_WAY -> {
currentTitle = "🚗 السائق في الطريق إليك"
currentBody = "$driverName • $carDetails • $etaText"
showNotification(
title = currentTitle,
body = currentBody,
progress = currentProgress,
maxProgress = 100,
indeterminate = false,
ongoing = true
)
render(indeterminate = false)
}
STATUS_DRIVER_ARRIVED -> {
currentTitle = "📍 السائق وصل"
currentBody = "الرجاء التوجّه لمقابلة $driverName عند نقطة الالتقاء"
showNotification(
title = currentTitle,
body = currentBody,
progress = 100,
maxProgress = 100,
indeterminate = false,
ongoing = true
)
currentProgress = 100
render(indeterminate = false)
}
STATUS_IN_PROGRESS -> {
currentTitle = "🚀 الرحلة جارية الآن"
currentBody = "المتبقي تقريبًا: $etaText"
showNotification(
title = currentTitle,
body = currentBody,
progress = currentProgress,
maxProgress = 100,
indeterminate = false,
ongoing = true
)
render(indeterminate = false)
}
}
}
@@ -208,29 +200,58 @@ class AndroidLiveUpdatePlugin(private val context: Context) {
}
/**
* إظهار الإشعار مع الإعدادات المناسبة
* يختار المسار المناسب: Live Update الحقيقية إن كانت متاحة ومسموحة،
* وإلا الإشعار الكلاسيكي الآمن على كل الإصدارات من API 23.
*/
private fun showNotification(
title: String,
body: String,
progress: Int,
maxProgress: Int,
indeterminate: Boolean,
ongoing: Boolean
) {
// Intent لفتح التطبيق عند النقر على الإشعار
val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra("notification_ride", true)
private fun render(indeterminate: Boolean) {
if (canUsePromotedLiveUpdate()) {
try {
showPromotedProgressNotification(indeterminate)
return
} catch (e: Exception) {
// أي فشل غير متوقع بالمسار الجديد يرجعنا فوراً للمسار الآمن
}
}
showClassicNotification(indeterminate)
}
val pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
/**
* المسار الجديد: Notification.ProgressStyle الحقيقية (Android 16 / API 36+).
* التوقيعات هنا مؤكدة مباشرة من ملف android-36/android.jar (javap)، وليست تخميناً.
*/
@RequiresApi(36)
private fun showPromotedProgressNotification(indeterminate: Boolean) {
val trackerIcon = Icon.createWithResource(context, android.R.drawable.ic_menu_directions)
val progressStyle = Notification.ProgressStyle()
.setProgressSegments(listOf(
Notification.ProgressStyle.Segment(100).setColor(statusColor())
))
.setProgress(currentProgress)
.setProgressIndeterminate(indeterminate)
.setProgressTrackerIcon(trackerIcon)
.setStyledByProgress(true)
val notification = Notification.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(currentTitle)
.setContentText(currentBody)
.setStyle(progressStyle)
.setCategory(Notification.CATEGORY_PROGRESS)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setContentIntent(buildContentIntent())
.setFlag(Notification.FLAG_PROMOTED_ONGOING, true)
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
}
/**
* المسار الكلاسيكي: إشعار ثابت بشريط تقدّم عادي، متوافق من API 23 وما فوق
* (وبدون قناة إشعارات على الإصدارات الأقدم من API 26).
*/
private fun showClassicNotification(indeterminate: Boolean) {
val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setLargeIcon(
@@ -239,30 +260,23 @@ class AndroidLiveUpdatePlugin(private val context: Context) {
android.R.drawable.ic_menu_directions
)
)
.setContentTitle(title)
.setContentText(body)
.setContentTitle(currentTitle)
.setContentText(currentBody)
.setStyle(
NotificationCompat.BigTextStyle()
.bigText(body)
.bigText(currentBody)
)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(ongoing)
.setOngoing(true)
.setAutoCancel(false)
.setOnlyAlertOnce(true)
.setContentIntent(pendingIntent)
.setContentIntent(buildContentIntent())
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
// تطبيق Progress Style
if (indeterminate) {
notificationBuilder.setProgress(0, 0, true)
} else {
notificationBuilder.setProgress(maxProgress, progress, false)
}
// Android 16: إضافة إجراءات سريعة للإشعار
if (Build.VERSION.SDK_INT >= 36) {
// يمكن إضافة إجراءات Android 16 هنا
// مثل: إظهار الخريطة، مشاركة الرحلة، إلخ
notificationBuilder.setProgress(currentMaxProgress, currentProgress, false)
}
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
@@ -302,7 +316,9 @@ class AndroidLiveUpdatePlugin(private val context: Context) {
result.success(true)
}
"isSupported" -> {
result.success(Build.VERSION.SDK_INT >= 30) // يدعم Android 11+
// الإشعار الكلاسيكي يعمل من API 23 وما فوق؛ الأجهزة الأحدث
// تحصل تلقائياً على Live Update الحقيقية عبر canUsePromotedLiveUpdate().
result.success(Build.VERSION.SDK_INT >= 23)
}
else -> {
result.notImplemented()
@@ -11,7 +11,6 @@ import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.view.setPadding
import com.scottyab.rootbeer.RootBeer
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
@@ -131,7 +130,7 @@ class MainActivity : FlutterFragmentActivity() {
private fun isDeviceCompromised(): Boolean {
return try {
nativeRootCheck() || rootBeerCheck() // || !safetyNetCheck()
nativeRootCheck() // || !safetyNetCheck()
} catch (e: Exception) {
Log.e("SecurityCheck", "Error during security checks: ${e.message}", e)
true
@@ -150,14 +149,6 @@ class MainActivity : FlutterFragmentActivity() {
}
}
private fun rootBeerCheck(): Boolean {
Log.d("SecurityCheck", "Starting RootBeer root detection...")
val rootBeer = RootBeer(this)
val isRooted = rootBeer.isRooted
Log.d("SecurityCheck", "RootBeer detection result: $isRooted")
return isRooted
}
// SafetyNet (معلّق كما هو)
private fun safetyNetCheck(): Boolean {
Log.d("SecurityCheck", "Starting SafetyNet check...")
+9 -9
View File
@@ -80,28 +80,28 @@ PODS:
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleUtilities/AppDelegateSwizzler (8.1.1):
- GoogleUtilities/AppDelegateSwizzler (8.1.2):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.1):
- GoogleUtilities/Environment (8.1.2):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.1):
- GoogleUtilities/Logger (8.1.2):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.1):
- GoogleUtilities/Network (8.1.2):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.1)":
- "GoogleUtilities/NSData+zlib (8.1.2)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.1)
- GoogleUtilities/Reachability (8.1.1):
- GoogleUtilities/Privacy (8.1.2)
- GoogleUtilities/Reachability (8.1.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.1):
- GoogleUtilities/UserDefaults (8.1.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMSessionFetcher/Core (5.3.0)
@@ -319,7 +319,7 @@ SPEC CHECKSUMS:
flutter_webrtc: ec91d94b484ad49cf191ef93413f64a40ffd3b4c
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 4f2618a4a1e762a1ee134a1e2323bba9843e06da
GoogleUtilities: 766ace00c6b10d8148408f329d10c4f051931850
GTMSessionFetcher: 127211aeec0b1e904fc49f4f6f895dcc535b0ecf
image_cropper: 64567491beea6cd1bc4b11948e2babb590de5826
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
+172
View File
@@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'colors.dart';
/// -----------------------------------------------------------------------------
/// Siro Design Tokens
/// -----------------------------------------------------------------------------
/// A single source of truth for spacing, radii, shadows, durations and the
/// shared bottom-sheet surface used across the passenger map overlays.
///
/// Goal: kill the "magic numbers" scattered across every map widget
/// (radii of 15/24/25, ad-hoc shadows, handles that appear only sometimes)
/// so all sheets read as ONE consistent product — the way Uber / Careem / Bolt
/// feel like a single system rather than a pile of separate screens.
///
/// Everything here is additive. Existing widgets keep working untouched; new
/// and refactored widgets opt in.
/// 4pt spacing scale. Use these instead of raw `SizedBox(height: 10)`.
class AppSpacing {
AppSpacing._();
static const double xs = 4;
static const double sm = 8;
static const double md = 12;
static const double lg = 16;
static const double xl = 20;
static const double xxl = 24;
// Convenience gaps (const so they can live in const child lists).
static const Widget gapXs = SizedBox(height: xs, width: xs);
static const Widget gapSm = SizedBox(height: sm, width: sm);
static const Widget gapMd = SizedBox(height: md, width: md);
static const Widget gapLg = SizedBox(height: lg, width: lg);
static const Widget gapXl = SizedBox(height: xl, width: xl);
static const Widget vGapXs = SizedBox(height: xs);
static const Widget vGapSm = SizedBox(height: sm);
static const Widget vGapMd = SizedBox(height: md);
static const Widget vGapLg = SizedBox(height: lg);
static const Widget vGapXl = SizedBox(height: xl);
}
/// Corner radii. `sheet` is THE radius for every bottom sheet so they all match.
class AppRadii {
AppRadii._();
static const double sm = 8;
static const double md = 12;
static const double lg = 16;
static const double sheet = 24;
static const double pill = 999;
static const BorderRadius sheetTop = BorderRadius.only(
topLeft: Radius.circular(sheet),
topRight: Radius.circular(sheet),
);
static BorderRadius all(double r) => BorderRadius.circular(r);
}
/// Shared elevation. One shadow for sheets, one for cards — no more per-widget
/// hand-tuned `BoxShadow`s (and no neumorphic accent-color shadows).
class AppShadows {
AppShadows._();
static List<BoxShadow> get sheet => [
BoxShadow(
color: Colors.black.withValues(
alpha: Get.isDarkMode ? 0.45 : 0.12,
),
blurRadius: 24,
spreadRadius: 1,
offset: const Offset(0, -4),
),
];
static List<BoxShadow> get card => [
BoxShadow(
color: Colors.black.withValues(
alpha: Get.isDarkMode ? 0.30 : 0.06,
),
blurRadius: 12,
offset: const Offset(0, 4),
),
];
}
/// Motion durations. Keep sheet slide-in/out consistent everywhere.
class AppDurations {
AppDurations._();
static const Duration fast = Duration(milliseconds: 200);
static const Duration base = Duration(milliseconds: 300);
static const Duration slow = Duration(milliseconds: 450);
}
/// Minimum touch target (Material / iOS HIG guidance is 44–48px).
class AppSizes {
AppSizes._();
static const double minTouch = 48;
}
/// -----------------------------------------------------------------------------
/// Shared bottom-sheet surface
/// -----------------------------------------------------------------------------
/// The rounded-top card + shadow + optional drag handle that every map sheet
/// draws by hand today. Wrap your content in this and all sheets instantly
/// share the same radius, shadow, handle and horizontal rhythm.
///
/// It does NOT own show/hide animation — each caller keeps its own
/// AnimatedPositioned / transform logic so behaviour is unchanged.
class SiroSheetSurface extends StatelessWidget {
const SiroSheetSurface({
super.key,
required this.child,
this.padding =
const EdgeInsets.fromLTRB(AppSpacing.xl, AppSpacing.md, AppSpacing.xl, AppSpacing.lg),
this.showHandle = true,
this.color,
});
final Widget child;
final EdgeInsets padding;
final bool showHandle;
final Color? color;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: color ?? AppColor.cardColor,
borderRadius: AppRadii.sheetTop,
boxShadow: AppShadows.sheet,
),
padding: padding,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (showHandle) ...[
const SiroSheetHandle(),
AppSpacing.vGapMd,
],
child,
],
),
);
}
}
/// The little grab handle at the top of a sheet. Consistent size everywhere.
class SiroSheetHandle extends StatelessWidget {
const SiroSheetHandle({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColor.grayColor.withValues(alpha: 0.35),
borderRadius: BorderRadius.circular(AppRadii.pill),
),
),
);
}
}
@@ -20,7 +20,7 @@ import '../../../print.dart';
import '../../../services/offline_map_service.dart';
import '../../functions/crud.dart';
import '../points_for_rider_controller.dart';
import '../../../views/home/map_widget.dart/form_serch_multiy_point.dart';
import '../../../views/home/map_widget.dart/waypoint_stops_list.dart';
import '../../../views/widgets/error_snakbar.dart';
import 'map_engine_controller.dart';
import '../deep_link_controller.dart';
@@ -29,6 +29,8 @@ class UiInteractionsController extends GetxController {
TextEditingController whatsAppLocationText = TextEditingController();
final sosFormKey = GlobalKey<FormState>();
@override
void onInit() {
super.onInit();
@@ -15,7 +15,7 @@ import '../../views/home/map_widget.dart/ride_begin_passenger.dart';
import '../../controller/home/menu_controller.dart';
import 'map_widget.dart/apply_order_widget.dart';
import 'map_widget.dart/buttom_sheet_map_show.dart';
import 'map_widget.dart/legacy_destination_bottom_sheet.dart';
import 'map_widget.dart/car_details_widget_to_go.dart';
import 'map_widget.dart/cash_confirm_bottom_page.dart';
import 'map_widget.dart/google_map_passenger_widget.dart';
@@ -23,8 +23,8 @@ import 'map_widget.dart/left_main_menu_icons.dart';
import 'route_planner/route_planner.dart';
import 'map_widget.dart/map_menu_widget.dart';
import '../../controller/functions/package_info.dart';
import 'map_widget.dart/passengerRideLoctionWidget.dart';
import 'map_widget.dart/payment_method.page.dart';
import 'map_widget.dart/passenger_ride_location_widget.dart';
import 'map_widget.dart/payment_method_page.dart';
import 'map_widget.dart/points_page_for_rider.dart';
import 'map_widget.dart/ride_from_start_app.dart';
import 'map_widget.dart/searching_captain_window.dart';
@@ -65,35 +65,26 @@ class MapPagePassenger extends StatelessWidget {
// OsmMapPassengerWidget(),
leftMainMenuIcons(),
// PaymobPackage(),
// ── New Route Planner ──
const RpCenterPin(),
// PickerAnimtionContainerFormPlaces(),
const RoutePlannerSheet(),
// NewMainBottomSheet(),
buttomSheetMapPage(),
CarDetailsTypeToChoose(),
// const HeaderDestination(),
const BurcMoney(),
// const PromoCode(),
ApplyOrderWidget(),
const MapMenuWidget(),
// hexagonClipper(),
const CancelRidePageShow(),
CashConfirmPageShown(),
const PaymentMethodPage(),
const SearchingCaptainWindow(),
AttributionMap(),
// timerForCancelTripFromPassenger(),
// const DriverTimeArrivePassengerPage(),
// const TimerToPassengerFromDriver(),
const PassengerRideLocationWidget(),
const RideBeginPassenger(),
const VipRideBeginPassenger(),
const RideFromStartApp(),
PointsPageForRider(),
// cancelRidePage(),
// const MenuIconMapPageWidget(),
PointsPageForRider()
],
),
),
@@ -171,8 +162,8 @@ class CancelRidePageShow extends StatelessWidget {
return showCancelButton
? Positioned(
right: box.read(BoxName.lang) != 'ar' ? 10 : null,
left: box.read(BoxName.lang) == 'ar' ? 10 : null,
right: box.read(BoxName.lang) == 'ar' ? 10 : null,
left: box.read(BoxName.lang) != 'ar' ? 10 : null,
top: Get.height * .013,
child: GestureDetector(
onTap: () {
@@ -11,6 +11,7 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../../../constant/box_name.dart';
import '../../../constant/design.dart';
import '../../../controller/firebase/notification_service.dart';
import '../../../controller/functions/launch.dart';
import '../../../controller/functions/crud.dart';
@@ -46,52 +47,30 @@ class ApplyOrderWidget extends StatelessWidget {
bottom: isVisible ? 0 : -400,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: const BorderRadius.vertical(top: Radius.circular(25)),
boxShadow: [
BoxShadow(
blurRadius: 20,
spreadRadius: 1,
color: Colors.black.withOpacity(0.1),
offset: const Offset(0, -3),
)
],
),
// تغيير: تقليل الحواف الخارجية بشكل كبير
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: SiroSheetSurface(
color: Theme.of(context).cardColor,
padding: const EdgeInsets.fromLTRB(
AppSpacing.lg, AppSpacing.sm, AppSpacing.lg, AppSpacing.lg),
child: GetBuilder<RideLifecycleController>(
builder: (c) {
return Column(
mainAxisSize:
MainAxisSize.min, // مهم جداً: يأخذ أقل مساحة ممكنة
children: [
// مقبض صغير
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.3),
borderRadius: BorderRadius.circular(10),
),
),
const SizedBox(height: 10), // تقليل المسافة
// 1. [تغيير جوهري] دمج السعر مع الحالة في صف واحد لتوفير المساحة
// 1. دمج السعر مع الحالة في صف واحد لتوفير المساحة
_buildCompactHeaderRow(context, c),
const SizedBox(height: 10), // مسافة مضغوطة
AppSpacing.vGapMd,
// 2. كرت المعلومات المضغوط
_buildCompactInfoCard(context, c, parseColor),
const SizedBox(height: 10), // مسافة مضغوطة
AppSpacing.vGapMd,
// 3. أزرار الاتصال (Slim)
_buildCompactButtonsRow(context, c),
const SizedBox(height: 10), // مسافة مضغوطة
AppSpacing.vGapMd,
// 4. شريط الوقت
c.currentRideState.value == RideState.driverArrived
@@ -240,10 +219,10 @@ class ApplyOrderWidget extends StatelessWidget {
RideLifecycleController controller, Color Function(String) parseColor) {
return Container(
// تقليل الحواف الداخلية للكرت
padding: const EdgeInsets.all(10),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(16),
borderRadius: AppRadii.all(AppRadii.lg),
border: Border.all(color: Colors.grey.withOpacity(0.1)),
),
child: Column(
@@ -496,7 +475,7 @@ class ApplyOrderWidget extends StatelessWidget {
Widget _buildCompactButtonsRow(
BuildContext context, RideLifecycleController controller) {
return SizedBox(
height: 40, // تحديد ارتفاع ثابت وصغير للأزرار
height: AppSizes.minTouch, // ارتفاع لمس قياسي 48px
child: Row(
children: [
Expanded(
@@ -508,7 +487,7 @@ class ApplyOrderWidget extends StatelessWidget {
onTap: () => _showContactOptionsDialog(context, controller),
),
),
const SizedBox(width: 10), // تقليل المسافة
AppSpacing.gapMd,
Expanded(
child: _buildSlimButton(
label: 'Call'.tr, // اختصار الكلمة
@@ -609,7 +588,8 @@ class ApplyOrderWidget extends StatelessWidget {
foregroundColor: color,
elevation: isPrimary ? 2 : 0,
padding: EdgeInsets.zero, // إزالة الحواشي الداخلية
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadii.md)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -1,258 +0,0 @@
// import 'dart:async';
// import 'package:SEFER/constant/box_name.dart';
// import 'package:SEFER/controller/home/map_passenger_controller.dart';
// import 'package:SEFER/main.dart';
// import 'package:SEFER/views/widgets/my_scafold.dart';
// import 'package:flutter/material.dart';
// import 'package:get/get.dart';
// import 'package:permission_handler/permission_handler.dart';
// import 'package:agora_rtc_engine/agora_rtc_engine.dart';
// import '../../../../constant/api_key.dart';
// import '../../../constant/colors.dart';
// import '../../../constant/style.dart';
// import '../../../controller/firebase/firbase_messge.dart';
// String appId = AK.agoraAppId;
// class PassengerCallPage extends StatefulWidget {
// const PassengerCallPage({
// super.key,
// required this.channelName,
// required this.token,
// required this.remoteID,
// });
// final String channelName, token, remoteID;
// @override
// State<PassengerCallPage> createState() => _PassengerCallPageState();
// }
// class _PassengerCallPageState extends State<PassengerCallPage> {
// int uid = 0;
// int? _remoteUid = 0; // uid of the remote user
// bool _isJoined = false; // Indicates if the local user has joined the channel
// late RtcEngine agoraEngine; // Agora engine instance
// String status = '';
// final GlobalKey<ScaffoldMessengerState> scaffoldMessengerKey =
// GlobalKey<ScaffoldMessengerState>(); // Global key to access the scaffold
// showMessage(String message) {
// scaffoldMessengerKey.currentState?.showSnackBar(SnackBar(
// content: Text(message),
// ));
// }
// initAgora() async {
// await setupVoiceSDKEngine();
// }
// @override
// void initState() {
// super.initState();
// _remoteUid = int.parse(widget.remoteID);
// uid = int.parse(box.read(BoxName.phone));
// // Set up an instance of Agora engine
// initAgora();
// }
// Future<void> setupVoiceSDKEngine() async {
// // retrieve or request microphone permission
// await [Permission.microphone].request();
// //create an instance of the Agora engine
// agoraEngine = createAgoraRtcEngine();
// await agoraEngine.initialize(RtcEngineContext(appId: AK.agoraAppId));
// // Register the event handler
// agoraEngine.registerEventHandler(
// RtcEngineEventHandler(
// onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
// showMessage(
// "Local user uid:${connection.localUid} joined the channel");
// setState(() {
// _isJoined = true;
// status = 'joined'.tr;
// });
// },
// onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
// showMessage("Driver joined the channel".tr);
// setState(() {
// status = "Driver joined the channel".tr;
// _remoteUid = remoteUid;
// });
// },
// onUserOffline: (RtcConnection connection, int? remoteUid,
// UserOfflineReasonType reason) {
// showMessage("Driver left the channel".tr);
// setState(() {
// status = "Driver left the channel".tr;
// _remoteUid = null;
// });
// },
// ),
// );
// }
// void join() async {
// // Set channel options including the client role and channel profile
// ChannelMediaOptions options = const ChannelMediaOptions(
// clientRoleType: ClientRoleType.clientRoleBroadcaster,
// channelProfile: ChannelProfileType.channelProfileCommunication,
// );
// await agoraEngine.joinChannel(
// token: widget.token,
// channelId: widget.channelName,
// options: options,
// uid: uid,
// );
// }
// //https://console.agora.io/invite?sign=5e9e22d06f22caeeada9954c9e908572%253A5ba8aed978a35eab5a5113742502ded2a41478b2a81cb19c71a30776e125b58a
// void leave() {
// setState(() {
// _isJoined = false;
// _remoteUid = null;
// });
// agoraEngine.leaveChannel();
// }
// // Clean up the resources when you leave
// @override
// void dispose() async {
// await agoraEngine.leaveChannel();
// super.dispose();
// }
// // Build UI
// @override
// Widget build(BuildContext context) {
// return MaterialApp(
// scaffoldMessengerKey: scaffoldMessengerKey,
// home: MyScafolld(
// // appBar: AppBar(
// // title: const Text('Get started with Voice Calling'),
// // ),
// title: 'Call Page'.tr,
// isleading: true,
// body: [
// Positioned(
// top: Get.height * .2,
// child: Container(
// height: 100, width: Get.width,
// decoration: AppStyle.boxDecoration,
// child: Row(
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// children: [
// GestureDetector(
// onTap: () async {
// // await callController.initAgoraFull();
// // callController.join();
// // FirebaseMessagesController().sendNotificationToPassengerToken(
// // 'Call Income',
// // '${'You have call from driver'.tr} ${box.read(BoxName.nameDriver)}',
// // Get.find<MapDriverController>().tokenPassenger,
// // [
// // callController.token,
// // callController.channelName,
// // callController.uid.toString(),
// // callController.remoteUid.toString(),
// // ],
// // );
// join();
// // callController.fetchToken();
// },
// child: Container(
// width: 50,
// height: 50,
// decoration: const BoxDecoration(
// shape: BoxShape.circle,
// color: AppColor.greenColor),
// child: const Icon(
// Icons.phone,
// size: 35,
// color: AppColor.secondaryColor,
// )),
// ),
// Column(
// children: [
// Text(
// status,
// style: AppStyle.title,
// ),
// Text('Driver Name'),
// ],
// ),
// GestureDetector(
// onTap: () async {
// FirebaseMessagesController()
// .sendNotificationToPassengerToken(
// 'Call End'.tr,
// 'Call End',
// Get.find<MapPassengerController>().driverToken,
// [],
// 'iphone_ringtone.wav',
// );
// leave();
// Get.back();
// // },
// child: Container(
// width: 50,
// height: 50,
// decoration: const BoxDecoration(
// shape: BoxShape.circle, color: AppColor.redColor),
// child: const Icon(
// Icons.phone_disabled_sharp,
// size: 35,
// color: AppColor.secondaryColor,
// )),
// )
// ],
// ),
// // ignore: prefer_const_constructors
// ),
// ),
// // ListView(
// // padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
// // children: [
// // // Status text
// // Container(height: 40, child: Center(child: _status())),
// // // Button Row
// // Row(
// // children: <Widget>[
// // Expanded(
// // child: ElevatedButton(
// // child: Text("Join".tr),
// // onPressed: () => {join()},
// // ),
// // ),
// // const SizedBox(width: 10),
// // Expanded(
// // child: ElevatedButton(
// // child: Text("Leave".tr),
// // onPressed: () => {leave()},
// // ),
// // ),
// // ],
// // ),
// // ],
// // ),
// ]),
// );
// }
// // Widget _status() {
// // String statusText;
// //
// // if (!_isJoined) {
// // statusText = 'Join a channel'.tr;
// // } else if (_remoteUid == null)
// // statusText = 'Waiting for a remote user to join...';
// // else
// // statusText = 'Connected to remote user, uid:$_remoteUid';
// //
// // return Text(
// // statusText,
// // );
// // }
// }
@@ -1,6 +1,5 @@
import 'package:siro_rider/constant/currency.dart';
import 'package:flutter/material.dart';
import 'package:siro_rider/controller/home/map/map_engine_controller.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/colors.dart';
@@ -9,9 +8,9 @@ import 'package:siro_rider/main.dart';
import 'package:siro_rider/views/home/profile/passenger_profile_page.dart';
import 'package:siro_rider/views/widgets/elevated_btn.dart';
import 'package:siro_rider/views/widgets/my_textField.dart';
import 'package:intl/intl.dart';
import 'dart:ui';
import '../../../constant/design.dart';
import '../../../constant/info.dart';
import '../../../controller/functions/tts.dart';
import '../../../controller/home/map/ride_lifecycle_controller.dart';
@@ -31,32 +30,56 @@ class CarType {
{required this.carType, required this.carDetail, required this.image});
}
List<CarType> carTypes = [
CarType(
// ─────────────────────────────────────────────────────────────────────────────
// CAR TYPE CATALOG (per-country availability)
// ─────────────────────────────────────────────────────────────────────────────
// كل نوع سيارة معرَّف مرة واحدة هنا؛ الدولة تحدد أي الأنواع تظهر للراكب.
final Map<String, CarType> _carTypeCatalog = {
'Fixed Price': CarType(
carType: 'Fixed Price',
carDetail: 'Closest & Cheapest'.tr,
image: 'assets/images/carspeed.png'),
CarType(
'Comfort': CarType(
carType: 'Comfort',
carDetail: 'Comfort choice'.tr,
image: 'assets/images/blob.png'),
CarType(
'Electric': CarType(
carType: 'Electric',
carDetail: 'Quiet & Eco-Friendly'.tr,
image: 'assets/images/electric.png'),
CarType(
'Lady': CarType(
carType: 'Lady',
carDetail: 'Lady Captain for girls'.tr,
image: 'assets/images/lady.png'),
CarType(
'Van': CarType(
carType: 'Van',
carDetail: 'Van for familly'.tr,
image: 'assets/images/bus.png'),
CarType(
carType: 'Rayeh Gai',
carDetail: "Best choice for cities".tr,
image: 'assets/images/roundtrip.png'),
];
'Scooter': CarType(
carType: 'Scooter',
carDetail: 'Motorcycle for one'.tr,
image: 'assets/images/moto.png'),
'Awfar Car': CarType(
carType: 'Awfar Car',
carDetail: 'Old and affordable'.tr,
image: 'assets/images/balash.png'),
};
// ترتيب وتوفر أنواع السيارات حسب الدولة (بدون "Rayeh Gai" لأنها تُضاف
// تلقائياً حسب مسافة الرحلة بغض النظر عن الدولة).
const Map<String, List<String>> _carTypesByCountry = {
'Jordan': ['Fixed Price', 'Comfort', 'Electric', 'Lady'],
'Egypt': ['Fixed Price', 'Comfort', 'Lady', 'Scooter', 'Awfar Car'],
'Syria': ['Fixed Price', 'Comfort', 'Electric', 'Lady', 'Van'],
};
List<CarType> _carTypesForCountry(String country) {
final keys = _carTypesByCountry[country] ?? _carTypesByCountry['Jordan']!;
return keys.map((k) => _carTypeCatalog[k]!).toList();
}
List<CarType> carTypes = _carTypesForCountry(
box.read(BoxName.countryCode) ?? 'Jordan');
// ─────────────────────────────────────────────────────────────────────────────
// MAIN WIDGET
@@ -65,7 +88,15 @@ class CarDetailsTypeToChoose extends StatelessWidget {
CarDetailsTypeToChoose({super.key});
final textToSpeechController = Get.find<TextToSpeechController>();
static String? _lastPreparedCountry;
void _prepareCarTypes(RideLifecycleController controller) {
final String country = box.read(BoxName.countryCode) ?? 'Jordan';
if (_lastPreparedCountry != country) {
carTypes = _carTypesForCountry(country);
_lastPreparedCountry = country;
}
if (controller.distance > 23) {
if (!carTypes.any((car) => car.carType == 'Rayeh Gai')) {
carTypes.add(CarType(
@@ -92,43 +123,24 @@ class CarDetailsTypeToChoose extends StatelessWidget {
left: 0,
right: 0,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
borderRadius: AppRadii.sheetTop,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
child: Container(
decoration: BoxDecoration(
color: AppColor.secondaryColor.withAlpha(240),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(30),
blurRadius: 30,
spreadRadius: 0,
offset: const Offset(0, -8),
),
],
borderRadius: AppRadii.sheetTop,
boxShadow: AppShadows.sheet,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ── Drag Handle ──────────────────────────────────────
Center(
child: Container(
width: 32,
height: 3,
margin: const EdgeInsets.only(top: 6, bottom: 2),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
const Padding(
padding: EdgeInsets.only(top: AppSpacing.sm),
child: SiroSheetHandle(),
),
AppSpacing.vGapXs,
// ── Header ───────────────────────────────────────────
_buildHeader(controller),
@@ -250,7 +262,8 @@ class CarDetailsTypeToChoose extends StatelessWidget {
controller.update();
},
child: Container(
padding: const EdgeInsets.all(5),
padding: const EdgeInsets.all(9),
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
decoration: BoxDecoration(
color: Get.isDarkMode
? Colors.white.withOpacity(0.08)
@@ -294,258 +307,45 @@ class CarDetailsTypeToChoose extends StatelessWidget {
}
// ═══════════════════════════════════════════════════════════════════════════
// CAR CARD
// CAR CARD (delegates rendering to _CarTypeCard)
// ═══════════════════════════════════════════════════════════════════════════
Widget _buildCarCard(BuildContext context, RideLifecycleController controller,
CarType carType, bool isSelected, int index) {
return GestureDetector(
return _CarTypeCard(
carType: carType,
isSelected: isSelected,
priceText:
'${_getPassengerPriceText(carType, controller)} ${CurrencyHelper.currency}',
onTap: () {
controller.selectCarFromList(index);
_showCarDetailsDialog(
context, controller, carType, textToSpeechController);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
width: 88,
decoration: BoxDecoration(
gradient: isSelected
? LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColor.primaryColor.withAlpha(22),
AppColor.primaryColor.withAlpha(10),
],
)
: null,
color: isSelected ? null : AppColor.secondaryColor,
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: isSelected
? AppColor.primaryColor
: AppColor.grayColor.withOpacity(0.25),
width: isSelected ? 2 : 1.2,
),
boxShadow: [
BoxShadow(
color: isSelected
? AppColor.primaryColor.withAlpha(50)
: Colors.black.withAlpha(12),
blurRadius: isSelected ? 10 : 4,
offset: const Offset(0, 2),
),
],
),
child: Stack(
children: [
// Selected indicator
if (isSelected)
Positioned(
top: 3,
right: 3,
child: Container(
width: 18,
height: 18,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColor.primaryColor,
AppColor.primaryColor.withAlpha(200),
],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppColor.primaryColor.withAlpha(60),
blurRadius: 4,
),
],
),
child: const Icon(Icons.check, size: 11, color: Colors.white),
),
),
// Card content
Padding(
padding: const EdgeInsets.fromLTRB(6, 6, 6, 6),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Car image
AnimatedScale(
scale: isSelected ? 1.1 : 1.0,
duration: const Duration(milliseconds: 300),
child: Image.asset(
carType.image,
height: 34,
fit: BoxFit.contain,
),
),
const SizedBox(height: 1),
// Car name
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
carType.carType.tr,
style: TextStyle(
fontWeight:
isSelected ? FontWeight.w800 : FontWeight.w600,
fontSize: 11,
color: isSelected
? AppColor.primaryColor
: AppColor.writeColor,
),
maxLines: 1,
),
),
const SizedBox(height: 1),
// Price tag
Container(
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: isSelected
? AppColor.primaryColor
: AppColor.writeColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
border: isSelected
? null
: Border.all(
color: AppColor.grayColor.withOpacity(0.2)),
),
child: FittedBox(
child: Text(
'${_getPassengerPriceText(carType, controller)} ${CurrencyHelper.currency}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w800,
color: isSelected
? Colors.white
: AppColor.writeColor.withOpacity(0.8),
),
),
),
),
],
),
),
],
),
),
);
}
// ═══════════════════════════════════════════════════════════════════════════
// PROMO BUTTON
// PROMO BUTTON (delegates rendering to _PromoCodeBanner)
// ═══════════════════════════════════════════════════════════════════════════
Widget _buildPromoButton(
BuildContext context, RideLifecycleController controller) {
if (controller.promoTaken) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.fromLTRB(16, 2, 16, 0),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _showPromoCodeDialog(context, controller),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 10),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.amber.shade50,
Colors.orange.shade50,
],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.amber.shade200),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: Colors.amber.shade100,
shape: BoxShape.circle,
),
child: Icon(Icons.percent_rounded,
color: Colors.amber.shade800, size: 14),
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Have a Promo Code?'.tr,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Colors.amber.shade900,
),
),
),
Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: Colors.amber.shade100,
borderRadius: BorderRadius.circular(6),
),
child: Icon(Icons.arrow_forward_ios_rounded,
size: 10, color: Colors.amber.shade800),
),
],
),
),
),
),
return _PromoCodeBanner(
onTap: () => _showPromoCodeDialog(context, controller),
);
}
// ═══════════════════════════════════════════════════════════════════════════
// NEGATIVE BALANCE WARNING
// NEGATIVE BALANCE WARNING (delegates rendering to _NegativeBalanceBanner)
// ═══════════════════════════════════════════════════════════════════════════
Widget _buildNegativeBalanceWarning(RideLifecycleController controller) {
final passengerWallet =
double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ?? 0.0;
if (passengerWallet < 0.0) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 3),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.red.shade100,
shape: BoxShape.circle,
),
child: Icon(Icons.warning_amber_rounded,
color: Colors.red.shade700, size: 14),
),
const SizedBox(width: 8),
Expanded(
child: Text(
'${'You have a negative balance of'.tr} ${passengerWallet.toStringAsFixed(2)} ${CurrencyHelper.currency}.',
style: TextStyle(
color: Colors.red.shade800,
fontWeight: FontWeight.w600,
fontSize: 11,
),
),
),
],
),
);
}
return const SizedBox.shrink();
if (passengerWallet >= 0.0) return const SizedBox.shrink();
return _NegativeBalanceBanner(
message:
'${'You have a negative balance of'.tr} ${passengerWallet.toStringAsFixed(2)} ${CurrencyHelper.currency}.',
);
}
// ═══════════════════════════════════════════════════════════════════════════
@@ -852,12 +652,12 @@ class CarDetailsTypeToChoose extends StatelessWidget {
case 'Electric':
return 'Travel in a modern, silent electric car. A premium, eco-friendly choice for a smooth ride.'
.tr;
case 'Scooter':
case 'Van':
return "Spacious van service ideal for families and groups. Comfortable, safe, and cost-effective travel together."
.tr;
case 'Scooter':
case 'Pink Bike':
return 'This is for delivery or a motorcycle.'.tr;
return 'A quick, affordable motorcycle ride for one.'.tr;
case 'Mishwar Vip':
return "Perfect for passengers seeking the latest car models with the freedom to choose any route they desire"
.tr;
@@ -939,6 +739,9 @@ class CarDetailsTypeToChoose extends StatelessWidget {
return mapPassengerController.totalPassengerVan;
case 'Lady':
return mapPassengerController.totalPassengerLady;
case 'Scooter':
case 'Pink Bike':
return mapPassengerController.totalPassengerScooter;
default:
return '0';
}
@@ -968,6 +771,281 @@ class CarDetailsTypeToChoose extends StatelessWidget {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// CAR TYPE CARD (selectable ride option in the horizontal list)
// ─────────────────────────────────────────────────────────────────────────────
class _CarTypeCard extends StatelessWidget {
const _CarTypeCard({
required this.carType,
required this.isSelected,
required this.priceText,
required this.onTap,
});
final CarType carType;
final bool isSelected;
final String priceText;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: AppDurations.base,
curve: Curves.easeOutCubic,
width: 88,
decoration: BoxDecoration(
gradient: isSelected
? LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColor.primaryColor.withAlpha(22),
AppColor.primaryColor.withAlpha(10),
],
)
: null,
color: isSelected ? null : AppColor.secondaryColor,
borderRadius: AppRadii.all(AppRadii.lg),
border: Border.all(
color: isSelected
? AppColor.primaryColor
: AppColor.grayColor.withOpacity(0.25),
width: isSelected ? 2 : 1.2,
),
boxShadow: [
BoxShadow(
color: isSelected
? AppColor.primaryColor.withAlpha(50)
: Colors.black.withAlpha(12),
blurRadius: isSelected ? 10 : 4,
offset: const Offset(0, 2),
),
],
),
child: Stack(
children: [
// Selected indicator
if (isSelected)
Positioned(
top: 3,
right: 3,
child: Container(
width: 18,
height: 18,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColor.primaryColor,
AppColor.primaryColor.withAlpha(200),
],
),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: AppColor.primaryColor.withAlpha(60),
blurRadius: 4,
),
],
),
child: const Icon(Icons.check, size: 11, color: Colors.white),
),
),
// Card content
Padding(
padding: const EdgeInsets.all(6),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Car image
AnimatedScale(
scale: isSelected ? 1.1 : 1.0,
duration: AppDurations.base,
child: Image.asset(
carType.image,
height: 34,
fit: BoxFit.contain,
),
),
const SizedBox(height: 1),
// Car name
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
carType.carType.tr,
style: TextStyle(
fontWeight:
isSelected ? FontWeight.w800 : FontWeight.w600,
fontSize: 11,
color: isSelected
? AppColor.primaryColor
: AppColor.writeColor,
),
maxLines: 1,
),
),
const SizedBox(height: 1),
// Price tag
Container(
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: isSelected
? AppColor.primaryColor
: AppColor.writeColor.withOpacity(0.05),
borderRadius: AppRadii.all(AppRadii.sm),
border: isSelected
? null
: Border.all(
color: AppColor.grayColor.withOpacity(0.2)),
),
child: FittedBox(
child: Text(
priceText,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w800,
color: isSelected
? Colors.white
: AppColor.writeColor.withOpacity(0.8),
),
),
),
),
],
),
),
],
),
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// PROMO CODE BANNER
// ─────────────────────────────────────────────────────────────────────────────
class _PromoCodeBanner extends StatelessWidget {
const _PromoCodeBanner({required this.onTap});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(AppSpacing.lg, 2, AppSpacing.lg, 0),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: AppRadii.all(AppRadii.md),
child: Container(
constraints: const BoxConstraints(minHeight: 44),
padding: const EdgeInsets.symmetric(
vertical: AppSpacing.sm, horizontal: AppSpacing.md),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.amber.shade50,
Colors.orange.shade50,
],
),
borderRadius: AppRadii.all(AppRadii.md),
border: Border.all(color: Colors.amber.shade200),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: Colors.amber.shade100,
shape: BoxShape.circle,
),
child: Icon(Icons.percent_rounded,
color: Colors.amber.shade800, size: 14),
),
AppSpacing.gapSm,
Expanded(
child: Text(
'Have a Promo Code?'.tr,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Colors.amber.shade900,
),
),
),
Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: Colors.amber.shade100,
borderRadius: AppRadii.all(AppRadii.sm),
),
child: Icon(Icons.arrow_forward_ios_rounded,
size: 10, color: Colors.amber.shade800),
),
],
),
),
),
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// NEGATIVE BALANCE BANNER
// ─────────────────────────────────────────────────────────────────────────────
class _NegativeBalanceBanner extends StatelessWidget {
const _NegativeBalanceBanner({required this.message});
final String message;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg, vertical: 3),
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.md, vertical: AppSpacing.sm),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: AppRadii.all(AppRadii.sm),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.red.shade100,
shape: BoxShape.circle,
),
child: Icon(Icons.warning_amber_rounded,
color: Colors.red.shade700, size: 14),
),
AppSpacing.gapSm,
Expanded(
child: Text(
message,
style: TextStyle(
color: Colors.red.shade800,
fontWeight: FontWeight.w600,
fontSize: 11,
),
),
),
],
),
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// BURC MONEY WIDGET (Floating negative balance banner)
// ─────────────────────────────────────────────────────────────────────────────
@@ -6,6 +6,7 @@ import 'package:siro_rider/constant/style.dart';
import 'package:siro_rider/views/home/my_wallet/passenger_wallet.dart';
import '../../../constant/colors.dart';
import '../../../constant/design.dart';
import '../../../constant/info.dart';
import '../../../controller/home/map/ride_lifecycle_controller.dart';
import '../../../controller/payment/payment_controller.dart';
@@ -25,26 +26,12 @@ class CashConfirmPageShown extends StatelessWidget {
left: 0,
right: 0,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
duration: AppDurations.base,
curve: Curves.easeInOut,
// التحكم في ظهور اللوحة لم يتغير
transform: Matrix4.translationValues(
0, controller.isCashConfirmPageShown ? 0 : Get.height, 0),
decoration: BoxDecoration(
color: AppColor.secondaryColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 20,
),
],
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: SiroSheetSurface(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
@@ -55,7 +42,7 @@ class CashConfirmPageShown extends StatelessWidget {
children: [
Text(
'Payment Method'.tr,
style: AppStyle.headTitle.copyWith(fontSize: 24),
style: AppStyle.headTitle.copyWith(fontSize: 22),
),
// زر الإغلاق (كان معلقاً في الكود القديم، تم تفعيله هنا)
IconButton(
@@ -64,7 +51,7 @@ class CashConfirmPageShown extends StatelessWidget {
),
],
),
const SizedBox(height: 16),
AppSpacing.vGapLg,
// --- 2. بطاقات اختيار الدفع ---
GetBuilder<PaymentController>(builder: (paymentCtrl) {
@@ -87,7 +74,7 @@ class CashConfirmPageShown extends StatelessWidget {
onTap: () =>
paymentCtrl.onChangedPaymentMethodWallet(true),
),
const SizedBox(height: 12),
AppSpacing.vGapMd,
// بطاقة الكاش
_buildPaymentOptionCard(
icon: Icons.money_rounded,
@@ -101,7 +88,7 @@ class CashConfirmPageShown extends StatelessWidget {
],
);
}),
const SizedBox(height: 24),
AppSpacing.vGapXl,
// --- 3. أزرار التأكيد (بنفس منطقك القديم تماماً) ---
GetBuilder<PaymentController>(builder: (paymentCtrl) {
@@ -165,13 +152,14 @@ class CashConfirmPageShown extends StatelessWidget {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
padding: const EdgeInsets.all(16),
duration: AppDurations.fast,
padding: const EdgeInsets.all(AppSpacing.lg),
constraints: const BoxConstraints(minHeight: AppSizes.minTouch),
decoration: BoxDecoration(
color: isSelected
? selectedColor.withOpacity(0.1)
: AppColor.writeColor.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(AppRadii.md),
border: Border.all(
color: isSelected
? selectedColor
@@ -1,106 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/style.dart';
import '../../../controller/home/map/ride_lifecycle_controller.dart';
import 'hexegone_clipper.dart';
GetBuilder<RideLifecycleController> hexagonClipper() {
return GetBuilder<RideLifecycleController>(
builder: ((controller) => controller.rideConfirm
? Positioned(
top: Get.height * .1,
left: Get.width * .1,
right: Get.width * .1,
child: ClipPath(
clipper:
HexagonClipper(), // CustomClipper to create a hexagon shape
child: AnimatedContainer(
duration: const Duration(microseconds: 300),
height: 250,
width: 250,
// decoration: AppStyle.boxDecoration,
// gradient: const LinearGradient(
// colors: [AppColor.greenColor, AppColor.secondaryColor],
// begin: Alignment.topLeft,
// end: Alignment.bottomCenter,
// ),
// border: Border.all(),
// color: AppColor.secondaryColor,
// borderRadius: BorderRadius.circular(15)),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Waiting for Driver ...'.tr,
style: AppStyle.title,
),
// IconButton(
// onPressed: () {
// },
// icon: const Icon(Icons.add),
// ),
// Text(
// controller.dataCarsLocationByPassenger['message']
// [controller.carsOrder]['phone']
// .toString(),
// style: AppStyle.title,
// ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
'${controller.dataCarsLocationByPassenger['message'][controller.carsOrder]['first_name']} ${controller.dataCarsLocationByPassenger['message'][controller.carsOrder]['last_name']}',
style: AppStyle.title,
),
Text(
'Age is '.tr +
controller
.dataCarsLocationByPassenger['message']
[controller.carsOrder]['age']
.toString(),
style: AppStyle.title,
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
controller.dataCarsLocationByPassenger['message']
[controller.carsOrder]['make']
.toString(),
style: AppStyle.title,
),
Text(
controller.dataCarsLocationByPassenger['message']
[controller.carsOrder]['model']
.toString(),
style: AppStyle.title,
),
],
),
Text(
'Rating is '.tr +
controller.dataCarsLocationByPassenger['message']
[controller.carsOrder]['ratingDriver']
.toString(),
style: AppStyle.title,
),
Container(
decoration: BoxDecoration(border: Border.all(width: 2)),
child: Text(
controller.dataCarsLocationByPassenger['message']
[controller.carsOrder]['car_plate']
.toString(),
style: AppStyle.title,
),
),
],
),
),
),
)
: const SizedBox()));
}
@@ -1,51 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
// import 'package:intl/intl.dart';
import '../../../constant/style.dart';
import '../../../controller/home/map/ride_lifecycle_controller.dart';
class DriverTimeArrivePassengerPage extends StatelessWidget {
const DriverTimeArrivePassengerPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<RideLifecycleController>(
builder: (controller) {
return controller.remainingTime == 0
? Positioned(
bottom: Get.height * .35,
right: Get.width * .05,
child: Stack(
alignment: Alignment.center,
children: [
Container(
decoration: AppStyle.boxDecoration,
// width: 50,
// height: 50,
child: Padding(
padding: const EdgeInsetsDirectional.only(
start: 5, end: 5),
child: Column(
children: [
Text(
controller.durationByPassenger.toString() +
' to arrive you.'.tr,
style: AppStyle.title,
),
Text(
" ${DateFormat('h:mm a').format(controller.newTime)}",
style: AppStyle.title,
),
],
),
))
],
),
)
: const SizedBox();
},
);
}
}
@@ -1,533 +0,0 @@
import 'package:siro_rider/print.dart';
import 'package:siro_rider/views/widgets/mydialoug.dart';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/table_names.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../../controller/functions/toast.dart';
import '../../../controller/home/map/location_search_controller.dart';
import '../../../controller/home/map/map_engine_controller.dart';
import '../../../controller/home/map/ride_lifecycle_controller.dart';
import '../../../main.dart';
// ---------------------------------------------------
// -- Widget for Destination Point Search (Optimized) --
// ---------------------------------------------------
GetBuilder<LocationSearchController> formSearchPlacesDestenation() {
final String addWorkValue =
box.read(BoxName.addWork)?.toString() ?? 'addWork';
final String addHomeValue =
box.read(BoxName.addHome)?.toString() ?? 'addHome';
if (addWorkValue.isEmpty || addHomeValue.isEmpty) {
box.write(BoxName.addWork, 'addWork');
box.write(BoxName.addHome, 'addHome');
}
return GetBuilder<LocationSearchController>(
id: 'destination_form',
builder: (controller) {
final mapEngine = Get.find<MapEngineController>();
final rideLifecycle = Get.find<RideLifecycleController>();
return Column(
children: [
_SearchField(
controller: controller,
mapEngine: mapEngine,
rideLifecycle: rideLifecycle,
),
_QuickActions(
controller: controller,
mapEngine: mapEngine,
rideLifecycle: rideLifecycle,
addWorkValue: addWorkValue,
addHomeValue: addHomeValue,
),
_SearchResults(
controller: controller,
mapEngine: mapEngine,
rideLifecycle: rideLifecycle,
),
],
);
},
);
}
// ---------------------------------------------------
// -- Private Helper Widgets for Cleaner Code --
// ---------------------------------------------------
class _SearchField extends StatefulWidget {
final LocationSearchController controller;
final MapEngineController mapEngine;
final RideLifecycleController rideLifecycle;
const _SearchField({
required this.controller,
required this.mapEngine,
required this.rideLifecycle,
});
@override
State<_SearchField> createState() => _SearchFieldState();
}
class _SearchFieldState extends State<_SearchField> {
Timer? _debounce;
void _onTextChanged() {
if (mounted) {
setState(() {});
}
}
@override
void initState() {
super.initState();
widget.controller.placeDestinationController.addListener(_onTextChanged);
}
void _onSearchChanged(String query) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
if (query.length > 2) {
widget.controller.getPlaces();
widget.mapEngine.changeHeightPlaces();
} else if (query.isEmpty) {
widget.controller.clearPlacesDestination();
widget.mapEngine.changeHeightPlaces();
}
});
}
@override
void dispose() {
_debounce?.cancel();
widget.controller.placeDestinationController.removeListener(_onTextChanged);
super.dispose();
}
@override
Widget build(BuildContext context) {
const Color accent = Color(0xFFEF4444); // matches the red destination dot
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0),
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
gradient: LinearGradient(colors: [
accent.withOpacity(0.08),
accent.withOpacity(0.03),
]),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: accent.withOpacity(0.35), width: 1.4),
),
child: Row(
children: [
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: accent,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: accent.withOpacity(0.4),
blurRadius: 10,
offset: const Offset(0, 3)),
],
),
child:
const Icon(Icons.flag_rounded, color: Colors.white, size: 17),
),
const SizedBox(width: 10),
Expanded(
child: TextFormField(
controller: widget.controller.placeDestinationController,
onChanged: _onSearchChanged,
style: const TextStyle(
fontSize: 14.5, fontWeight: FontWeight.w700),
decoration: InputDecoration(
hintText: widget.controller.hintTextDestinationPoint,
hintStyle: AppStyle.subtitle.copyWith(
color: accent.withOpacity(0.85),
fontWeight: FontWeight.w700,
fontSize: 14.5,
),
isDense: true,
suffixIcon: widget
.controller.placeDestinationController.text.isNotEmpty
? IconButton(
icon: Icon(Icons.clear, color: Colors.grey[400]),
onPressed: () {
widget.controller.placeDestinationController
.clear();
},
)
: null,
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
focusedBorder: InputBorder.none,
filled: false,
),
),
),
const SizedBox(width: 6.0),
InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
widget.mapEngine.changeMainBottomMenuMap();
widget.mapEngine.changePickerShown();
},
child: Tooltip(
message: widget.rideLifecycle.isAnotherOreder
? 'Pick destination on map'.tr
: 'Pick on map'.tr,
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: accent.withOpacity(0.3)),
),
child: Icon(Icons.map_rounded, color: accent, size: 18),
),
),
),
],
),
),
);
}
}
class _QuickActions extends StatelessWidget {
final LocationSearchController controller;
final MapEngineController mapEngine;
final RideLifecycleController rideLifecycle;
final String addWorkValue;
final String addHomeValue;
const _QuickActions({
required this.controller,
required this.mapEngine,
required this.rideLifecycle,
required this.addWorkValue,
required this.addHomeValue,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildQuickActionButton(
icon: Icons.work_outline,
text: addWorkValue == 'addWork' ? 'Add Work'.tr : 'To Work'.tr,
onTap: () {
if (addWorkValue == 'addWork') {
controller.workLocationFromMap = true;
mapEngine.changeMainBottomMenuMap();
mapEngine.changePickerShown();
} else {
_handleQuickAction(
controller,
mapEngine,
rideLifecycle,
BoxName.addWork,
'To Work',
);
}
},
onLongPress: () =>
_showChangeLocationDialog(controller, mapEngine, 'Work'),
),
_buildQuickActionButton(
icon: Icons.home_outlined,
text: addHomeValue == 'addHome' ? 'Add Home'.tr : 'To Home'.tr,
onTap: () {
if (addHomeValue == 'addHome') {
controller.homeLocationFromMap = true;
mapEngine.changeMainBottomMenuMap();
mapEngine.changePickerShown();
} else {
_handleQuickAction(
controller,
mapEngine,
rideLifecycle,
BoxName.addHome,
'To Home',
);
}
},
onLongPress: () =>
_showChangeLocationDialog(controller, mapEngine, 'Home'),
),
],
),
);
}
}
class _SearchResults extends StatelessWidget {
final LocationSearchController controller;
final MapEngineController mapEngine;
final RideLifecycleController rideLifecycle;
const _SearchResults({
required this.controller,
required this.mapEngine,
required this.rideLifecycle,
});
@override
Widget build(BuildContext context) {
return GetBuilder<LocationSearchController>(
id: 'places_list',
builder: (locCtrl) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: locCtrl.placesDestination.isNotEmpty ? 300 : 0,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.0),
),
margin: const EdgeInsets.symmetric(horizontal: 16.0),
child: ListView.separated(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: locCtrl.placesDestination.length,
separatorBuilder: (context, index) =>
const Divider(height: 1, color: Colors.grey),
itemBuilder: (BuildContext context, int index) {
final res = locCtrl.placesDestination[index];
final title = res['name_ar'] ?? res['name'] ?? 'Unknown Place';
final address = res['address'] ?? 'Details not available';
final latitude = res['latitude'];
final longitude = res['longitude'];
return ListTile(
leading: const Icon(Icons.place, size: 30, color: Colors.grey),
title: Text(
title,
style:
AppStyle.subtitle.copyWith(fontWeight: FontWeight.w500),
),
subtitle: Text(
address,
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
trailing: IconButton(
icon: const Icon(Icons.favorite_border, color: Colors.grey),
onPressed: () => _handleAddToFavorites(
context, latitude, longitude, title),
),
onTap: () => _handlePlaceSelection(
controller,
mapEngine,
rideLifecycle,
latitude,
longitude,
title,
index,
),
);
},
),
);
},
);
}
Future<void> _handleAddToFavorites(BuildContext context, dynamic latitude,
dynamic longitude, String title) async {
if (latitude != null && longitude != null) {
await sql.insertMapLocation({
'latitude': latitude,
'longitude': longitude,
'name': title,
'rate': 'N/A',
}, TableName.placesFavorite);
Toast.show(
context,
'$title ${'Saved Successfully'.tr}',
AppColor.primaryColor,
);
} else {
Toast.show(
context,
'Invalid location data',
AppColor.redColor,
);
}
}
Future<void> _handlePlaceSelection(
LocationSearchController controller,
MapEngineController mapEngine,
RideLifecycleController rideLifecycle,
dynamic latitude,
dynamic longitude,
String title,
int index) async {
if (latitude == null || longitude == null) {
Toast.show(Get.context!, 'Invalid location data', AppColor.redColor);
return;
}
await sql.insertMapLocation({
'latitude': latitude,
'longitude': longitude,
'name': title,
'rate': 'N/A',
'createdAt': DateTime.now().toIso8601String(),
}, TableName.recentLocations);
final destLatLng = LatLng(
double.parse(latitude.toString()), double.parse(longitude.toString()));
if (rideLifecycle.isAnotherOreder) {
await _handleAnotherOrderSelection(
controller, mapEngine, rideLifecycle, destLatLng);
} else {
_handleRegularOrderSelection(
controller, mapEngine, rideLifecycle, destLatLng, index);
}
}
Future<void> _handleAnotherOrderSelection(
LocationSearchController controller,
MapEngineController mapEngine,
RideLifecycleController rideLifecycle,
LatLng destination) async {
controller.myDestination = destination;
controller.clearPlacesDestination();
await rideLifecycle.getDirectionMap(
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
'${controller.myDestination.latitude},${controller.myDestination.longitude}');
mapEngine.isPickerShown = false;
controller.passengerStartLocationFromMap = false;
mapEngine.changeMainBottomMenuMap();
rideLifecycle.showBottomSheet1();
}
void _handleRegularOrderSelection(
LocationSearchController controller,
MapEngineController mapEngine,
RideLifecycleController rideLifecycle,
LatLng destination,
int index) {
controller.passengerLocation = controller.newMyLocation;
controller.myDestination = destination;
controller.convertHintTextDestinationNewPlaces(index);
controller.clearPlacesDestination();
mapEngine.changeMainBottomMenuMap();
controller.passengerStartLocationFromMap = true;
mapEngine.isPickerShown = true;
rideLifecycle.getDirectionMap(
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
'${controller.myDestination.latitude},${controller.myDestination.longitude}');
}
}
Widget _buildQuickActionButton({
required IconData icon,
required String text,
VoidCallback? onTap,
VoidCallback? onLongPress,
}) {
return InkWell(
onTap: onTap,
onLongPress: onLongPress,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: AppColor.cyanBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8.0),
border: Border.all(color: AppColor.cyanBlue.withOpacity(0.3)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: AppColor.cyanBlue),
const SizedBox(height: 4.0),
Text(
text,
textAlign: TextAlign.center,
style: AppStyle.title.copyWith(
color: AppColor.cyanBlue, fontWeight: FontWeight.w500),
),
],
),
),
);
}
void _showChangeLocationDialog(LocationSearchController controller,
MapEngineController mapEngine, String locationType) {
MyDialog().getDialog(
locationType == 'Work'
? 'Change Work location ?'.tr
: 'Change Home location ?'.tr,
'',
() {
if (locationType == 'Work') {
controller.workLocationFromMap = true;
} else {
controller.homeLocationFromMap = true;
}
mapEngine.changeMainBottomMenuMap();
mapEngine.changePickerShown();
},
);
}
void _handleQuickAction(
LocationSearchController controller,
MapEngineController mapEngine,
RideLifecycleController rideLifecycle,
String boxName,
String hintText) async {
try {
final locationString = box.read(boxName).toString();
final parts = locationString.split(',');
final latLng = LatLng(
double.parse(parts[0]),
double.parse(parts[1]),
);
controller.hintTextDestinationPoint = hintText;
mapEngine.changeMainBottomMenuMap();
await rideLifecycle.getDirectionMap(
'${controller.passengerLocation.latitude},${controller.passengerLocation.longitude}',
'${latLng.latitude},${latLng.longitude}',
);
controller.currentLocationToFormPlaces = false;
controller.clearPlacesDestination();
controller.passengerStartLocationFromMap = false;
mapEngine.isPickerShown = false;
rideLifecycle.showBottomSheet1();
} catch (e) {
Log.print("Error handling quick action: $e");
Toast.show(Get.context!, "Failed to get location".tr, AppColor.redColor);
}
}
@@ -1,165 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../../../constant/style.dart';
import '../../../controller/home/map/location_search_controller.dart';
import '../../../controller/home/map/map_engine_controller.dart';
// ---------------------------------------------------
// -- Widget for Start Point Search (Updated) --
// ---------------------------------------------------
GetBuilder<LocationSearchController> formSearchPlacesStart() {
return GetBuilder<LocationSearchController>(
id: 'start_point_form',
builder: (controller) {
final mapEngine = Get.find<MapEngineController>();
const Color accent = Color(0xFF16A34A); // green: "their" pickup point
return Column(
children: [
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0),
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
gradient: LinearGradient(colors: [
accent.withOpacity(0.08),
accent.withOpacity(0.03),
]),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: accent.withOpacity(0.35), width: 1.4),
),
child: Row(
children: [
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: accent,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: accent.withOpacity(0.4),
blurRadius: 10,
offset: const Offset(0, 3)),
],
),
child: const Icon(Icons.person_pin_circle_rounded,
color: Colors.white, size: 18),
),
const SizedBox(width: 10),
Expanded(
child: TextFormField(
controller: controller.placeStartController,
style: const TextStyle(
fontSize: 14.5, fontWeight: FontWeight.w700),
onChanged: (value) {
if (controller.placeStartController.text.length > 2) {
controller.getPlacesStart();
} else if (controller
.placeStartController.text.isEmpty) {
controller.clearPlacesStart();
}
},
decoration: InputDecoration(
hintText: 'Search for a starting point'.tr,
hintStyle: AppStyle.subtitle.copyWith(
color: accent.withOpacity(0.85),
fontWeight: FontWeight.w700,
fontSize: 14.5,
),
isDense: true,
suffixIcon: controller
.placeStartController.text.isNotEmpty
? IconButton(
icon:
Icon(Icons.clear, color: Colors.grey[400]),
onPressed: () {
controller.placeStartController.clear();
controller.clearPlacesStart();
},
)
: null,
contentPadding: EdgeInsets.zero,
border: InputBorder.none,
focusedBorder: InputBorder.none,
filled: false,
),
),
),
const SizedBox(width: 6.0),
InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
controller.passengerStartLocationFromMap = true;
mapEngine.changeMainBottomMenuMap();
mapEngine.changePickerShown();
},
child: Tooltip(
message: 'Pick start point on map'.tr,
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: accent.withOpacity(0.3)),
),
child: Icon(Icons.map_rounded, color: accent, size: 18),
),
),
),
],
),
),
),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: controller.placesStart.isNotEmpty ? 300 : 0,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.0),
),
margin: const EdgeInsets.symmetric(horizontal: 16.0),
child: ListView.separated(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: controller.placesStart.length,
separatorBuilder: (context, index) =>
const Divider(height: 1, color: Colors.grey),
itemBuilder: (BuildContext context, int index) {
var res = controller.placesStart[index];
var title = res['name_ar'] ?? res['name'] ?? 'Unknown Place';
var address = res['address'] ?? 'Details not available';
return ListTile(
leading:
const Icon(Icons.place, size: 30, color: Colors.grey),
title: Text(title,
style: AppStyle.subtitle
.copyWith(fontWeight: FontWeight.w500)),
subtitle: Text(address,
style: TextStyle(color: Colors.grey[600], fontSize: 12)),
onTap: () {
var latitude = res['latitude'];
var longitude = res['longitude'];
if (latitude != null && longitude != null) {
controller.passengerLocation = LatLng(
double.parse(latitude), double.parse(longitude));
controller.placeStartController.text = title;
controller.clearPlacesStart();
mapEngine.changeMainBottomMenuMap();
controller.update();
}
},
);
},
),
),
],
);
},
);
}
@@ -1,52 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
class HexagonClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
final height = size.height;
final width = size.width;
final centerX = width / 2;
final centerY = height / 2;
final radius = width / 2;
const angle = 2 * pi / 10; // Angle between each side of the hexagon
// Start at the top right vertex of the hexagon
final startX = centerX + radius * cos(0);
final startY = centerY + radius * sin(0);
path.moveTo(startX, startY);
// Draw the remaining sides of the hexagon
for (int i = 1; i < 10; i++) {
final x = centerX + radius * cos(angle * i);
final y = centerY + radius * sin(angle * i);
path.lineTo(x, y);
}
path.close();
return path;
}
@override
bool shouldReclip(HexagonClipper oldClipper) => false;
}
class ArrowClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.moveTo(0, size.height / 2);
path.lineTo(size.width / 2, 0);
path.lineTo(size.width, size.height / 2);
path.lineTo(size.width / 2, size.height);
path.close();
return path;
}
@override
bool shouldReclip(ArrowClipper oldClipper) => false;
}
@@ -7,12 +7,15 @@ import 'package:flutter_font_icons/flutter_font_icons.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import 'dart:ui';
import 'dart:io';
import '../../../constant/colors.dart';
import '../../../controller/home/map/location_search_controller.dart';
import '../../../controller/home/map/map_engine_controller.dart';
import '../../../controller/home/vip_waitting_page.dart';
import '../navigation/navigation_view.dart';
import '../../../services/android_live_update_service.dart';
import '../../../controller/home/ios_live_activity_service.dart';
// --- الدالة الرئيسية بالتصميم الجديد ---
GetBuilder<MapEngineController> leftMainMenuIcons() {
@@ -66,6 +69,11 @@ GetBuilder<MapEngineController> leftMainMenuIcons() {
tooltip: 'VIP Waiting Page',
onPressed: () => Get.to(() => VipWaittingPage()),
),
_buildMapActionButton(
icon: Icons.explore,
tooltip: 'Test Page',
onPressed: () => Get.to(() => TestPage()),
),
],
),
),
@@ -107,7 +115,7 @@ class TestPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('iOS Live Activity Test'),
title: const Text('Live Activity Test (iOS & Android)'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
@@ -116,14 +124,67 @@ class TestPage extends StatelessWidget {
children: [
MyCircularProgressIndicator(),
MyElevatedButton(
title: 'title',
onPressed: () {},
title: 'Start Live Activity Demo',
onPressed: () {
if (Platform.isIOS) {
IosLiveActivityService.startRideActivity(
rideId: "demo_trip_123",
driverName: "أحمد علي",
carDetails: "تويوتا كامري - أبيض",
etaText: "5 دقائق للوصول",
progress: 0.3,
);
} else if (Platform.isAndroid) {
AndroidLiveUpdateService.startLiveUpdate(
rideId: "demo_trip_123",
driverName: "أحمد علي",
carDetails: "تويوتا كامري - أبيض",
etaText: "5 دقائق للوصول",
progress: 0.3,
);
}
},
),
const SizedBox(height: 20),
MyElevatedButton(
title: 'Update Live Activity (Random)',
onPressed: () {
final random = Random();
final int minutes = random.nextInt(15) + 1;
final double progress = random.nextDouble();
final String newEtaText = "$minutes دقائق للوصول";
if (Platform.isIOS) {
IosLiveActivityService.updateRideActivity(
status: "accepted",
driverName: "أحمد علي",
carDetails: "تويوتا كامري - أبيض",
etaText: newEtaText,
progress: progress,
);
} else if (Platform.isAndroid) {
AndroidLiveUpdateService.updateStatus(
status: "accepted",
driverName: "أحمد علي",
carDetails: "تويوتا كامري - أبيض",
etaText: newEtaText,
progress: progress,
);
}
},
),
const SizedBox(height: 20),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
onPressed: () async {},
onPressed: () async {
if (Platform.isIOS) {
await IosLiveActivityService.endRideActivity();
} else if (Platform.isAndroid) {
await AndroidLiveUpdateService.endLiveUpdate();
}
},
child: const Text('End Activity'),
),
],
File diff suppressed because it is too large Load Diff
@@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../controller/home/map/map_engine_controller.dart';
import '../../../main.dart';
class MenuIconMapPageWidget extends StatelessWidget {
const MenuIconMapPageWidget({
super.key,
});
@override
Widget build(BuildContext context) {
return GetBuilder<MapEngineController>(
builder: (controller) => Positioned(
top: Get.height * .008,
left: box.read(BoxName.lang) != 'ar' ? 5 : null,
right: box.read(BoxName.lang) == 'ar' ? 5 : null,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.secondaryColor,
border: Border.all(color: AppColor.accentColor)),
child: AnimatedCrossFade(
sizeCurve: Curves.bounceOut,
duration: const Duration(
milliseconds: 300), // Adjust the duration as needed
crossFadeState: controller.heightMenuBool
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: IconButton(
onPressed: () {
controller.getDrawerMenu();
},
icon: const Icon(
Icons.close,
color: AppColor.primaryColor,
),
),
secondChild: IconButton(
onPressed: () {
controller.getDrawerMenu();
},
icon: const Icon(
Icons.menu,
color: AppColor.accentColor,
),
),
),
),
));
}
}
@@ -1,86 +0,0 @@
import 'package:siro_rider/constant/box_name.dart';
import 'package:siro_rider/constant/colors.dart';
import 'package:siro_rider/constant/style.dart';
import 'package:siro_rider/main.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class NewMainBottomSheet extends StatelessWidget {
const NewMainBottomSheet({super.key});
@override
Widget build(BuildContext context) {
return Positioned(
bottom: 0,
left: 5,
right: 5,
child: Container(
decoration: AppStyle.boxDecoration,
width: Get.width,
height: Get.height * .15,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(
decoration: BoxDecoration(
border: Border.all(),
borderRadius: BorderRadius.circular(15)),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Text('Home'.tr),
const Icon(Icons.home),
],
),
),
),
Container(
decoration: BoxDecoration(
border: Border.all(),
borderRadius: BorderRadius.circular(15)),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Text('Work'.tr),
const Icon(Icons.work_outline),
],
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
decoration: BoxDecoration(
border: Border.all(),
borderRadius: BorderRadius.circular(15),
color: AppColor.blueColor.withOpacity(.5),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Icon(Icons.search),
Text(
"${"Where you want go ".tr}${(box.read(BoxName.name).toString().split(' ')[0]).toString()} ?",
),
],
),
),
)
],
)
],
),
),
);
}
}
@@ -1,214 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/table_names.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../../controller/home/map/location_search_controller.dart';
import '../../../controller/home/map/map_engine_controller.dart';
import '../../../controller/home/map/ride_lifecycle_controller.dart';
import '../../../main.dart';
import '../../widgets/error_snakbar.dart';
import '../../widgets/elevated_btn.dart';
import 'form_search_places_destenation.dart';
class PickerAnimtionContainerFormPlaces extends StatelessWidget {
const PickerAnimtionContainerFormPlaces({super.key});
@override
Widget build(BuildContext context) {
final mapEngine = Get.find<MapEngineController>();
final locationSearch = Get.find<LocationSearchController>();
final rideLifecycle = Get.find<RideLifecycleController>();
return GetBuilder<MapEngineController>(
builder: (controller) => Positioned(
bottom: 0,
left: 0,
right: 5,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: controller.heightPickerContainer,
decoration: BoxDecoration(
boxShadow: const [
BoxShadow(
color: AppColor.accentColor, offset: Offset(2, 2)),
BoxShadow(
color: AppColor.accentColor, offset: Offset(-2, -2))
],
color: AppColor.secondaryColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(15),
topRight: Radius.circular(15),
)),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
controller.isPickerShown
? const SizedBox()
: Text(
'Hi, Where to '.tr,
style: AppStyle.title,
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const SizedBox(
height: 5,
),
controller.isPickerShown
? InkWell(
onTapDown: (details) {
controller.changePickerShown();
controller.changeHeightPlaces();
},
child: Container(
height: 7,
width: Get.width * .3,
decoration: BoxDecoration(
color: AppColor.accentColor,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppColor.accentColor,
)),
),
)
: const SizedBox(),
controller.isPickerShown
? InkWell(
onTap: () {},
child: formSearchPlacesDestenation(),
)
: Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () {
controller.changePickerShown();
},
child: Text(
"Pick your destination from Map".tr,
style: AppStyle.subtitle,
),
),
TextButton(
onPressed: () async {
List favoritePlaces = await sql
.getAllData(TableName.placesFavorite);
Get.defaultDialog(
title: 'Favorite Places'.tr,
content: SizedBox(
width: Get.width * .8,
height: 300,
child: favoritePlaces.isEmpty
? Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const Icon(
Icons
.hourglass_empty_rounded,
size: 99,
color: AppColor
.primaryColor,
),
Text(
'You Dont Have Any places yet !'
.tr,
style: AppStyle.title,
),
],
),
)
: ListView.builder(
itemCount:
favoritePlaces.length,
itemBuilder:
(BuildContext context,
int index) {
return Row(
mainAxisAlignment:
MainAxisAlignment
.spaceBetween,
children: [
TextButton(
onPressed: () async {
await rideLifecycle
.getDirectionMap(
'${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}',
'${favoritePlaces[index]['latitude']},${favoritePlaces[index]['longitude']}',
);
controller
.changePickerShown();
controller
.changeBottomSheetShown(
forceValue:
true);
rideLifecycle
.bottomSheet();
Get.back();
},
child: Text(
favoritePlaces[
index]['name'],
style:
AppStyle.title,
),
),
IconButton(
onPressed: () async {
await sql.deleteData(
TableName
.placesFavorite,
favoritePlaces[
index]
['id']);
Get.back();
mySnackbarInfo('${'You are Delete'.tr} ${favoritePlaces[index]['name']} from your list');
},
icon: const Icon(Icons
.favorite_outlined),
),
],
);
},
),
),
onCancel: () {},
);
},
child: Text(
"Go To Favorite Places".tr,
style: AppStyle.subtitle,
),
),
],
),
if (controller.isPickerShown &&
locationSearch.placesDestination.isEmpty)
MyElevatedButton(
title: 'Go to this Target'.tr,
onPressed: () async {
await rideLifecycle.getDirectionMap(
'${locationSearch.passengerLocation.latitude},${locationSearch.passengerLocation.longitude}',
'${locationSearch.newMyLocation.latitude},${locationSearch.newMyLocation.longitude}',
);
controller.changePickerShown();
controller.changeBottomSheetShown(
forceValue: true);
rideLifecycle.bottomSheet();
},
),
if (controller.isPickerShown &&
locationSearch.placesDestination.isEmpty)
const SizedBox(),
],
),
],
),
),
));
}
}
@@ -7,6 +7,7 @@ import 'package:intl/intl.dart';
// تأكد من المسارات
import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../constant/design.dart';
import '../../../constant/links.dart';
import 'package:siro_rider/controller/functions/country_logic.dart';
import '../../../controller/functions/audio_record1.dart';
@@ -18,7 +19,6 @@ import '../../../controller/home/map/ride_state.dart';
import '../../../controller/profile/profile_controller.dart';
import '../../../main.dart';
import '../../../views/home/profile/complaint_page.dart';
import '../../../controller/functions/country_logic.dart';
class RideBeginPassenger extends StatelessWidget {
const RideBeginPassenger({super.key});
@@ -28,7 +28,6 @@ class RideBeginPassenger extends StatelessWidget {
final ProfileController profileController = Get.put(ProfileController());
final AudioRecorderController audioController =
Get.put(AudioRecorderController());
final uiController = Get.find<UiInteractionsController>();
return Obx(() {
final controller = Get.find<RideLifecycleController>();
@@ -45,64 +44,33 @@ class RideBeginPassenger extends StatelessWidget {
bottom: isVisible ? 0 : -300,
left: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: AppColor.secondaryColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(25),
topRight: Radius.circular(25),
),
boxShadow: [
BoxShadow(
color: Get.isDarkMode
? Colors.black.withValues(alpha: 0.4)
: Colors.black.withValues(alpha: 0.1),
blurRadius: 20,
spreadRadius: 2,
offset: const Offset(0, -3),
),
child: SiroSheetSurface(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.lg, vertical: AppSpacing.md),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 1. هيدر المعلومات (سائق + سيارة + سعر)
_buildCompactHeader(controller),
AppSpacing.vGapMd,
// خط فاصل خفيف
Divider(
height: 1,
thickness: 0.5,
color: AppColor.grayColor.withValues(alpha: 0.2)),
AppSpacing.vGapMd,
// 2. الأزرار (إجراءات)
_buildCompactActionButtons(
context, controller, profileController, audioController),
// هامش سفلي بسيط لرفع الأزرار عن حافة الشاشة
AppSpacing.vGapXs,
],
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 1. مقبض السحب
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppColor.grayColor.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 12),
// 2. هيدر المعلومات (سائق + سيارة + سعر)
_buildCompactHeader(controller),
const SizedBox(height: 12),
// خط فاصل خفيف
Divider(
height: 1,
thickness: 0.5,
color: AppColor.grayColor.withValues(alpha: 0.2)),
const SizedBox(height: 12),
// 3. الأزرار (إجراءات)
_buildCompactActionButtons(
context, controller, profileController, audioController),
// إضافة هامش سفلي بسيط لرفع الأزرار عن حافة الشاشة
const SizedBox(height: 5),
],
),
),
),
);
});
@@ -227,7 +195,7 @@ class RideBeginPassenger extends StatelessWidget {
AudioRecorderController audioController) {
final uiController = Get.find<UiInteractionsController>();
return SizedBox(
height: 60,
height: 64,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
@@ -325,19 +293,20 @@ class RideBeginPassenger extends StatelessWidget {
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(AppRadii.md),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(8),
padding: const EdgeInsets.all(10),
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
decoration: BoxDecoration(
color: bgColor,
shape: BoxShape.circle,
),
child: Icon(icon, size: 20, color: color),
),
const SizedBox(height: 4),
AppSpacing.vGapXs,
Text(
label,
style: TextStyle(
@@ -1,11 +1,12 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/colors.dart';
import 'package:siro_rider/constant/design.dart';
import 'package:siro_rider/constant/style.dart';
import 'package:siro_rider/controller/home/map/ride_lifecycle_controller.dart';
import 'package:siro_rider/controller/home/map/ride_state.dart';
// --- الويدجت الرئيسية بالتصميم الجديد ---
// --- نافذة البحث عن كابتن ---
class SearchingCaptainWindow extends StatefulWidget {
const SearchingCaptainWindow({super.key});
@@ -34,60 +35,45 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
@override
Widget build(BuildContext context) {
// [تعديل 1] نستخدم Obx للاستماع إلى التغييرات في حالة الرحلة
// نستمع لتغييرات حالة الرحلة
return Obx(() {
// ابحث عن الكنترولر مرة واحدة
final controller = Get.find<RideLifecycleController>();
// [تعديل 2] شرط الإظهار يعتمد الآن على حالة الرحلة مباشرة
// شرط الإظهار يعتمد على حالة الرحلة مباشرة
final bool isVisible =
controller.currentRideState.value == RideState.searching;
return AnimatedPositioned(
duration: const Duration(milliseconds: 300),
duration: AppDurations.base,
curve: Curves.easeInOut,
bottom: isVisible ? 0 : -Get.height * 0.45, // زيادة الارتفاع قليلاً
bottom: isVisible ? 0 : -Get.height * 0.45,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
decoration: BoxDecoration(
color: AppColor.secondaryColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(24),
topRight: Radius.circular(24),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 20,
offset: const Offset(0, -5),
),
],
),
// لا يوجد مقبض سحب: هذه الحالة لا تُغلق بالسحب بل بزر الإلغاء فقط
child: SiroSheetSurface(
showHandle: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// --- 1. أنيميشن الرادار ---
_buildRadarAnimation(controller),
const SizedBox(height: 20),
AppSpacing.vGapXl,
// --- 2. زر الإلغاء ---
SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () {
// [تعديل 3] استدعاء دالة الإلغاء الموحدة
// استدعاء دالة الإلغاء الموحدة
controller.changeCancelRidePageShow();
// ();
},
style: OutlinedButton.styleFrom(
foregroundColor: AppColor.writeColor,
side:
BorderSide(color: AppColor.writeColor.withValues(alpha: 0.3)),
side: BorderSide(
color: AppColor.writeColor.withValues(alpha: 0.3)),
minimumSize: const Size.fromHeight(AppSizes.minTouch),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 12),
borderRadius: BorderRadius.circular(AppRadii.md)),
),
child: Text('Cancel Search'.tr),
),
@@ -106,7 +92,7 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
child: Stack(
alignment: Alignment.center,
children: [
// --- دوائر الرادار المتحركة (تبقى كما هي) ---
// --- دوائر الرادار المتحركة ---
...List.generate(3, (index) {
return FadeTransition(
opacity: Tween<double>(begin: 1.0, end: 0.0).animate(
@@ -139,25 +125,21 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
// [تعديل 4] النص يأتي مباشرة من الكنترولر
// النص يأتي مباشرة من الكنترولر
controller.driversStatusForSearchWindow,
style: AppStyle.headTitle.copyWith(fontSize: 20),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
AppSpacing.vGapSm,
Text(
'Searching for the nearest captain...'.tr,
style: AppStyle.subtitle
.copyWith(color: AppColor.writeColor.withValues(alpha: 0.7)),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// --- [!! تعديل جوهري !!] ---
// لم نعد بحاجة لـ buildTimerForIncrease
// المؤقت الرئيسي في الكنترولر هو من يقرر متى يعرض الحوار
// وهذا الجزء من الواجهة أصبح "غبياً" (لا يحتوي على منطق)
// الكنترولر سيستدعي _showIncreaseFeeDialog مباشرة
AppSpacing.vGapLg,
// مؤشر دوّار: المؤقت الرئيسي في الكنترولر هو من يقرر متى يعرض
// حوار زيادة الأجرة، لذا هذا الجزء "غبي" بلا منطق.
SizedBox(
height: 40,
width: 40,
@@ -167,7 +149,8 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
CircularProgressIndicator(
strokeWidth: 3,
color: AppColor.primaryColor,
backgroundColor: AppColor.primaryColor.withValues(alpha: 0.2),
backgroundColor:
AppColor.primaryColor.withValues(alpha: 0.2),
),
Center(
child: Icon(
@@ -185,286 +168,3 @@ class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
);
}
}
// --- [!! تعديل جوهري !!] ---
// تم حذف دالة `buildTimerForIncrease` بالكامل.
// تم حذف دالة `_showIncreaseFeeDialog` من هذا الملف.
// لماذا؟ لأن الكنترولر الآن هو المسؤول الوحيد عن إظهار الحوار.
// دالة `_showIncreaseFeeDialog` موجودة بالفعل داخل `map_passenger_controller.dart`
// وسيتم استدعاؤها من `_handleRideState` عند انتهاء مهلة الـ 90 ثانية.
// // --- الويدجت الرئيسية بالتصميم الجديد ---
// class SearchingCaptainWindow extends StatefulWidget {
// const SearchingCaptainWindow({super.key});
// @override
// State<SearchingCaptainWindow> createState() => _SearchingCaptainWindowState();
// }
// class _SearchingCaptainWindowState extends State<SearchingCaptainWindow>
// with SingleTickerProviderStateMixin {
// late AnimationController _animationController;
// @override
// void initState() {
// super.initState();
// _animationController = AnimationController(
// vsync: this,
// duration: const Duration(seconds: 2),
// )..repeat();
// }
// @override
// void dispose() {
// _animationController.dispose();
// super.dispose();
// }
// @override
// Widget build(BuildContext context) {
// return GetBuilder<MapPassengerController>(
// builder: (controller) {
// return AnimatedPositioned(
// duration: const Duration(milliseconds: 300),
// curve: Curves.easeInOut,
// bottom: controller.isSearchingWindow ? 0 : -Get.height * 0.4,
// left: 0,
// right: 0,
// child: Container(
// padding: const EdgeInsets.fromLTRB(20, 20, 20, 16),
// decoration: BoxDecoration(
// color: AppColor.secondaryColor,
// borderRadius: const BorderRadius.only(
// topLeft: Radius.circular(24),
// topRight: Radius.circular(24),
// ),
// boxShadow: [
// BoxShadow(
// color: Colors.black.withOpacity(0.2),
// blurRadius: 20,
// offset: const Offset(0, -5),
// ),
// ],
// ),
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// // --- 1. أنيميشن الرادار ---
// _buildRadarAnimation(controller),
// const SizedBox(height: 20),
// // --- 2. زر الإلغاء ---
// SizedBox(
// width: double.infinity,
// child: OutlinedButton(
// onPressed: () {
// // --- نفس منطقك للإلغاء ---
// controller.changeCancelRidePageShow();
// },
// style: OutlinedButton.styleFrom(
// foregroundColor: AppColor.writeColor,
// side: BorderSide(
// color: AppColor.writeColor.withOpacity(0.3)),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(12)),
// padding: const EdgeInsets.symmetric(vertical: 12),
// ),
// child: Text('Cancel Search'.tr),
// ),
// ),
// ],
// ),
// ),
// );
// },
// );
// }
// // --- ويدجت بناء أنيميشن الرادار ---
// Widget _buildRadarAnimation(MapPassengerController controller) {
// return SizedBox(
// height: 180, // ارتفاع ثابت لمنطقة الأنيميشن
// child: Stack(
// alignment: Alignment.center,
// children: [
// // --- دوائر الرادار المتحركة ---
// ...List.generate(3, (index) {
// return FadeTransition(
// opacity: Tween<double>(begin: 1.0, end: 0.0).animate(
// CurvedAnimation(
// parent: _animationController,
// curve: Interval((index) / 3, 1.0, curve: Curves.easeInOut),
// ),
// ),
// child: ScaleTransition(
// scale: Tween<double>(begin: 0.3, end: 1.0).animate(
// CurvedAnimation(
// parent: _animationController,
// curve: Interval((index) / 3, 1.0, curve: Curves.easeInOut),
// ),
// ),
// child: Container(
// decoration: BoxDecoration(
// shape: BoxShape.circle,
// border: Border.all(
// color: AppColor.primaryColor.withOpacity(0.7),
// width: 2,
// ),
// ),
// ),
// ),
// );
// }),
// // --- المحتوى في المنتصف ---
// Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text(
// controller.driversStatusForSearchWindow,
// style: AppStyle.headTitle.copyWith(fontSize: 20),
// textAlign: TextAlign.center,
// ),
// const SizedBox(height: 8),
// Text(
// 'Searching for the nearest captain...'.tr,
// style: AppStyle.subtitle
// .copyWith(color: AppColor.writeColor.withOpacity(0.7)),
// textAlign: TextAlign.center,
// ),
// const SizedBox(height: 16),
// // --- استدعاء نفس دالة المؤقت الخاصة بك ---
// buildTimerForIncrease(controller),
// ],
// ),
// ],
// ),
// );
// }
// }
// // --- نفس دالة المؤقت الخاصة بك مع تعديلات شكلية بسيطة ---
// Widget buildTimerForIncrease(MapPassengerController mapPassengerController) {
// return StreamBuilder<int>(
// stream: Stream.periodic(const Duration(seconds: 1))
// .map((_) => ++mapPassengerController.currentTimeSearchingCaptainWindow),
// initialData: 0,
// builder: (context, snapshot) {
// if (snapshot.hasData && snapshot.data! > 45) {
// // --- عرض زر زيادة الأجرة بنفس منطقك القديم ---
// return TextButton(
// onPressed: () =>
// _showIncreaseFeeDialog(context, mapPassengerController),
// child: Text(
// "No one accepted? Try increasing the fare.".tr,
// style: AppStyle.title.copyWith(
// color: AppColor.primaryColor,
// decoration: TextDecoration.underline),
// textAlign: TextAlign.center,
// ),
// );
// }
// final double progress = (snapshot.data ?? 0).toDouble() / 30.0;
// return SizedBox(
// height: 40,
// width: 40,
// child: Stack(
// fit: StackFit.expand,
// children: [
// CircularProgressIndicator(
// value: progress,
// strokeWidth: 3,
// color: AppColor.primaryColor,
// backgroundColor: AppColor.primaryColor.withOpacity(0.2),
// ),
// Center(
// child: Text(
// '${snapshot.data ?? 0}',
// style: AppStyle.title.copyWith(
// color: AppColor.writeColor, fontWeight: FontWeight.bold),
// ),
// ),
// ],
// ),
// );
// },
// );
// }
// // --- دالة لعرض نافذة زيادة الأجرة (مأخوذة من منطقك القديم) ---
// void _showIncreaseFeeDialog(
// BuildContext context, MapPassengerController mapPassengerController) {
// Get.defaultDialog(
// barrierDismissible: false,
// title: "Increase Your Trip Fee (Optional)".tr,
// titleStyle: AppStyle.title,
// content: Column(
// children: [
// Text(
// "We haven't found any drivers yet. Consider increasing your trip fee to make your offer more attractive to drivers."
// .tr,
// style: AppStyle.subtitle,
// textAlign: TextAlign.center,
// ),
// const SizedBox(height: 16),
// Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// IconButton(
// onPressed: () {
// mapPassengerController.increasFeeFromPassenger.text =
// (mapPassengerController.totalPassenger + 3)
// .toStringAsFixed(1);
// mapPassengerController.update();
// },
// icon: const Icon(Icons.add_circle,
// size: 40, color: AppColor.greenColor),
// ),
// SizedBox(
// width: 100,
// child: Form(
// key: mapPassengerController.increaseFeeFormKey,
// child: MyTextForm(
// controller: mapPassengerController.increasFeeFromPassenger,
// label:
// mapPassengerController.totalPassenger.toStringAsFixed(2),
// hint:
// mapPassengerController.totalPassenger.toStringAsFixed(2),
// type: TextInputType.number,
// ),
// ),
// ),
// IconButton(
// onPressed: () {
// mapPassengerController.increasFeeFromPassenger.text =
// (mapPassengerController.totalPassenger - 3)
// .toStringAsFixed(1);
// mapPassengerController.update();
// },
// icon: const Icon(Icons.remove_circle,
// size: 40, color: AppColor.redColor),
// ),
// ],
// ),
// ],
// ),
// actions: [
// TextButton(
// child: Text("No, thanks".tr,
// style: const TextStyle(color: AppColor.redColor)),
// onPressed: () {
// Get.back();
// // mapPassengerController.cancelRide();
// mapPassengerController.changeCancelRidePageShow();
// },
// ),
// ElevatedButton(
// style: ElevatedButton.styleFrom(backgroundColor: AppColor.greenColor),
// child: Text("Increase Fee".tr),
// onPressed: () =>
// mapPassengerController.increaseFeeByPassengerAndReOrder(),
// ),
// ],
// );
// }
@@ -46,49 +46,43 @@ class CupertinoDriverListWidget extends StatelessWidget {
child: CupertinoListTile(
padding: const EdgeInsets.symmetric(
vertical: 4, horizontal: 8),
leading: CircleAvatar(
radius: 25,
backgroundImage: NetworkImage(
'${AppLink.server}/portrate_captain_image/${driver['id']}.jpg',
),
child: Builder(
builder: (context) {
return Image.network(
'${AppLink.server}/portrate_captain_image/${driver['id']}.jpg',
fit: BoxFit.cover,
loadingBuilder: (BuildContext context,
Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) {
return child; // Image is loaded
} else {
return Center(
child: CircularProgressIndicator(
value: loadingProgress
.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
(loadingProgress
.expectedTotalBytes ??
1)
: null,
),
);
}
},
errorBuilder: (BuildContext context,
Object error, StackTrace? stackTrace) {
return const Icon(
Icons
.person, // Icon to show when image fails to load
size: 25, // Adjust the size as needed
color: AppColor
.blueColor, // Color for the error icon
);
},
);
},
leading: ClipOval(
child: SizedBox(
width: 50,
height: 50,
child: Image.network(
'${AppLink.server}/portrate_captain_image/${driver['id']}.jpg',
fit: BoxFit.cover,
loadingBuilder: (BuildContext context,
Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) {
return child; // Image is loaded
}
return Center(
child: CircularProgressIndicator(
strokeWidth: 2,
value: loadingProgress
.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
(loadingProgress
.expectedTotalBytes ??
1)
: null,
),
);
},
errorBuilder: (BuildContext context,
Object error, StackTrace? stackTrace) {
return const Icon(
Icons.person,
size: 25,
color: AppColor.blueColor,
);
},
),
),
),
title: Row(
@@ -1,63 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/style.dart';
import '../../../controller/home/map/ride_lifecycle_controller.dart';
GetBuilder<RideLifecycleController> timerForCancelTripFromPassenger() {
return GetBuilder<RideLifecycleController>(
builder: (controller) {
final isNearEnd =
controller.remainingTime <= 5; // Define a threshold for "near end"
return controller.remainingTime > 0 && controller.remainingTime != 25
? Positioned(
bottom: 5,
left: 10,
right: 10,
child: Container(
height: 180,
decoration: AppStyle.boxDecoration,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Stack(
alignment: Alignment.center,
children: [
CircularProgressIndicator(
value: controller.progress,
// Set the color based on the "isNearEnd" condition
color: isNearEnd ? Colors.red : Colors.blue,
),
Text(
'${controller.remainingTime}',
style: AppStyle.number,
),
],
),
const SizedBox(
width: 30,
),
Text(
'You can cancel Ride now'.tr,
style: AppStyle.title,
)
],
),
Text(
'After this period\nYou can\'t cancel!'.tr,
style: AppStyle.title,
)
],
),
),
),
)
: const SizedBox();
},
);
}
@@ -1,148 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/views/widgets/elevated_btn.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../../controller/home/map/ride_lifecycle_controller.dart';
import 'ride_begin_passenger.dart';
class TimerToPassengerFromDriver extends StatelessWidget {
const TimerToPassengerFromDriver({
super.key,
});
@override
Widget build(BuildContext context) {
return GetBuilder<RideLifecycleController>(builder: (controller) {
if (controller.remainingTime == 0 &&
(controller.isDriverInPassengerWay == true ||
controller.timeToPassengerFromDriverAfterApplied > 0)) {
// ) {
return Positioned(
left: 10,
right: 10,
bottom: 5,
child: Container(
decoration: AppStyle.boxDecoration,
height: controller.remainingTime == 0 &&
(controller.isDriverInPassengerWay == true ||
controller.timeToPassengerFromDriverAfterApplied > 0)
? 200
: 0,
// width: 100,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text(
'You Can cancel Ride After Captain did not come in the time'
.tr,
style: AppStyle.title,
textAlign: TextAlign.center,
),
Stack(
children: [
LinearProgressIndicator(
backgroundColor: AppColor.accentColor,
color: controller
.remainingTimeToPassengerFromDriverAfterApplied <
60
? AppColor.redColor
: AppColor.greenColor,
minHeight: 25,
borderRadius: BorderRadius.circular(15),
value: controller
.progressTimerToPassengerFromDriverAfterApplied
.toDouble(),
),
Center(
child: Text(
controller.stringRemainingTimeToPassenger,
style: AppStyle.title,
),
)
],
),
IconButton(
onPressed: () {},
icon: const Icon(
Icons.phone,
color: AppColor.blueColor,
),
),
controller.remainingTimeToPassengerFromDriverAfterApplied < 60
? MyElevatedButton(
title: 'You can cancel trip'.tr,
onPressed: () async {
await controller
.calculateDistanceBetweenPassengerAndDriverBeforeCancelRide();
})
: const SizedBox()
],
),
),
),
);
} else if (controller.remainingTime == 0 &&
controller.isDriverArrivePassenger == true) {
return Positioned(
left: 10,
right: 10,
bottom: 5,
child: Container(
decoration: AppStyle.boxDecoration,
height: controller.remainingTime == 0 &&
controller.isDriverArrivePassenger == true
? 150
: 0,
// width: 100,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text(
'The driver waiting you in picked location .'.tr,
style: AppStyle.title,
textAlign: TextAlign.center,
),
Stack(
children: [
LinearProgressIndicator(
backgroundColor: AppColor.accentColor,
color:
controller.remainingTimeDriverWaitPassenger5Minute <
60
? AppColor.redColor
: AppColor.greenColor,
minHeight: 50,
borderRadius: BorderRadius.circular(15),
value: controller
.progressTimerDriverWaitPassenger5Minute
.toDouble(),
),
Center(
child: Text(
controller
.stringRemainingTimeDriverWaitPassenger5Minute,
style: AppStyle.title,
),
)
],
),
Text(
'Please go to Car now '.tr,
style: AppStyle.title,
textAlign: TextAlign.center,
),
],
),
),
),
);
} else {
return const RideBeginPassenger();
}
});
}
}