2026-03-13-2
This commit is contained in:
@@ -263,6 +263,14 @@ class MapPassengerController extends GetxController {
|
|||||||
bool currentLocationToFormPlaces3 = false;
|
bool currentLocationToFormPlaces3 = false;
|
||||||
bool currentLocationToFormPlaces4 = false;
|
bool currentLocationToFormPlaces4 = false;
|
||||||
List currentLocationToFormPlacesAll = [];
|
List currentLocationToFormPlacesAll = [];
|
||||||
|
|
||||||
|
// ── Multi-Waypoint (max 2 stops) ──────────────────────────────────────────
|
||||||
|
List<LatLng?> menuWaypoints = [null, null];
|
||||||
|
List<String> menuWaypointNames = ['', ''];
|
||||||
|
int activeMenuWaypointCount = 0;
|
||||||
|
bool isPickingWaypoint = false;
|
||||||
|
int pickingWaypointIndex = -1;
|
||||||
|
|
||||||
late String driverToken = '';
|
late String driverToken = '';
|
||||||
int carsOrder = 0;
|
int carsOrder = 0;
|
||||||
int wayPointIndex = 0;
|
int wayPointIndex = 0;
|
||||||
@@ -4906,6 +4914,82 @@ Intaleq Team''';
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Multi-Waypoint Methods ──────────────────────────────────────────────────
|
||||||
|
void addMenuWaypoint() {
|
||||||
|
if (activeMenuWaypointCount >= 2) return;
|
||||||
|
activeMenuWaypointCount++;
|
||||||
|
// Increase expanded bottom menu height to accommodate new waypoint row
|
||||||
|
mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeMenuWaypoint(int index) {
|
||||||
|
if (index < 0 || index >= 2) return;
|
||||||
|
// Shift items if removing first waypoint while second exists
|
||||||
|
if (index == 0 && activeMenuWaypointCount == 2) {
|
||||||
|
menuWaypoints[0] = menuWaypoints[1];
|
||||||
|
menuWaypointNames[0] = menuWaypointNames[1];
|
||||||
|
}
|
||||||
|
menuWaypoints[activeMenuWaypointCount - 1] = null;
|
||||||
|
menuWaypointNames[activeMenuWaypointCount - 1] = '';
|
||||||
|
activeMenuWaypointCount--;
|
||||||
|
mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearAllMenuWaypoints() {
|
||||||
|
menuWaypoints = [null, null];
|
||||||
|
menuWaypointNames = ['', ''];
|
||||||
|
activeMenuWaypointCount = 0;
|
||||||
|
isPickingWaypoint = false;
|
||||||
|
pickingWaypointIndex = -1;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void startPickingWaypointOnMap(int index) {
|
||||||
|
pickingWaypointIndex = index;
|
||||||
|
isPickingWaypoint = true;
|
||||||
|
isPickerShown = true;
|
||||||
|
heightPickerContainer = 150;
|
||||||
|
// Close the expanded menu to show the map picker
|
||||||
|
isMainBottomMenuMap = true;
|
||||||
|
mainBottomMenuMapHeight = Get.height * .22;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMenuWaypointFromMap(int index, LatLng position) {
|
||||||
|
if (index < 0 || index >= 2) return;
|
||||||
|
menuWaypoints[index] = position;
|
||||||
|
menuWaypointNames[index] =
|
||||||
|
'${position.latitude.toStringAsFixed(4)}, ${position.longitude.toStringAsFixed(4)}';
|
||||||
|
isPickingWaypoint = false;
|
||||||
|
pickingWaypointIndex = -1;
|
||||||
|
isPickerShown = false;
|
||||||
|
// Re-open expanded menu
|
||||||
|
isMainBottomMenuMap = false;
|
||||||
|
mainBottomMenuMapHeight = Get.height * .6 + (activeMenuWaypointCount * 56);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMenuWaypointFromSearch(int index, LatLng pos, String name) {
|
||||||
|
if (index < 0 || index >= 2) return;
|
||||||
|
menuWaypoints[index] = pos;
|
||||||
|
menuWaypointNames[index] = name;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build OSRM waypoint coordinate string for the route URL
|
||||||
|
String _buildOsrmWaypointCoords() {
|
||||||
|
String coords = '';
|
||||||
|
for (int i = 0; i < activeMenuWaypointCount; i++) {
|
||||||
|
final wp = menuWaypoints[i];
|
||||||
|
if (wp != null) {
|
||||||
|
coords += ';${wp.longitude},${wp.latitude}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
void changeHeightPointsPageForRider() {
|
void changeHeightPointsPageForRider() {
|
||||||
isPointsPageForRider = !isPointsPageForRider;
|
isPointsPageForRider = !isPointsPageForRider;
|
||||||
heightPointsPageForRider = isPointsPageForRider == true ? Get.height : 0;
|
heightPointsPageForRider = isPointsPageForRider == true ? Get.height : 0;
|
||||||
@@ -6375,10 +6459,12 @@ Intaleq Team''';
|
|||||||
double lngDest = double.parse(coordDestination[1]);
|
double lngDest = double.parse(coordDestination[1]);
|
||||||
myDestination = LatLng(latDest, lngDest);
|
myDestination = LatLng(latDest, lngDest);
|
||||||
|
|
||||||
// 2. الاتصال بالسيرفر - New OSRM format
|
// 2. الاتصال بالسيرفر - New OSRM format (with multi-waypoint support)
|
||||||
var originCoords = origin.split(',');
|
var originCoords = origin.split(',');
|
||||||
|
// Build waypoint coordinates for OSRM (inserted between origin and destination)
|
||||||
|
String waypointCoords = _buildOsrmWaypointCoords();
|
||||||
String dynamicApiUrl =
|
String dynamicApiUrl =
|
||||||
'${AppLink.routesOsm}/route/v1/driving/${originCoords[1]},${originCoords[0]};$lngDest,$latDest';
|
'${AppLink.routesOsm}/route/v1/driving/${originCoords[1]},${originCoords[0]}$waypointCoords;$lngDest,$latDest';
|
||||||
|
|
||||||
var uri = Uri.parse('$dynamicApiUrl?steps=false&overview=full');
|
var uri = Uri.parse('$dynamicApiUrl?steps=false&overview=full');
|
||||||
Log.print('Requesting Route URI (Attempt: ${attemptCount + 1}): $uri');
|
Log.print('Requesting Route URI (Attempt: ${attemptCount + 1}): $uri');
|
||||||
@@ -6506,14 +6592,6 @@ Intaleq Team''';
|
|||||||
isDrawingRoute = false;
|
isDrawingRoute = false;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|
||||||
if (minLat != null) {
|
|
||||||
LatLngBounds boundsData = LatLngBounds(
|
|
||||||
northeast: LatLng(maxLat!, maxLng!),
|
|
||||||
southwest: LatLng(minLat!, minLng!));
|
|
||||||
mapController
|
|
||||||
?.animateCamera(CameraUpdate.newLatLngBounds(boundsData, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. إضافة الماركرز
|
// 6. إضافة الماركرز
|
||||||
durationToAdd = Duration(seconds: durationToRide);
|
durationToAdd = Duration(seconds: durationToRide);
|
||||||
hours = durationToAdd.inHours;
|
hours = durationToAdd.inHours;
|
||||||
@@ -6537,6 +6615,25 @@ Intaleq Team''';
|
|||||||
'$distance ${'KM'.tr} ⌛ ${hours > 0 ? '$hours H $minutes m' : '$minutes m'}'),
|
'$distance ${'KM'.tr} ⌛ ${hours > 0 ? '$hours H $minutes m' : '$minutes m'}'),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// 6b. Add waypoint markers (amber for stop 1, deep purple for stop 2)
|
||||||
|
for (int i = 0; i < activeMenuWaypointCount; i++) {
|
||||||
|
final wp = menuWaypoints[i];
|
||||||
|
if (wp != null) {
|
||||||
|
markers.add(Marker(
|
||||||
|
markerId: MarkerId('waypoint_$i'),
|
||||||
|
position: wp,
|
||||||
|
icon: BitmapDescriptor.defaultMarkerWithHue(
|
||||||
|
i == 0
|
||||||
|
? BitmapDescriptor.hueOrange
|
||||||
|
: BitmapDescriptor.hueViolet,
|
||||||
|
),
|
||||||
|
infoWindow: InfoWindow(
|
||||||
|
title: '${'Stop'.tr} ${i + 1}',
|
||||||
|
snippet: menuWaypointNames[i]),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 7. رسم الخط (النظام الجديد لجميع الأجهزة)
|
// 7. رسم الخط (النظام الجديد لجميع الأجهزة)
|
||||||
if (polyLines.isNotEmpty) clearPolyline();
|
if (polyLines.isNotEmpty) clearPolyline();
|
||||||
|
|
||||||
@@ -6547,8 +6644,16 @@ Intaleq Team''';
|
|||||||
// إظهار الباتم شيت للسعر
|
// إظهار الباتم شيت للسعر
|
||||||
bottomSheet();
|
bottomSheet();
|
||||||
|
|
||||||
// تشغيل الأنيميشن الخفيف لومضات المسار
|
// 8. Compute bounds from all polyline points
|
||||||
_playRouteAnimation(polylineCoordinates);
|
LatLngBounds? boundsData;
|
||||||
|
if (minLat != null) {
|
||||||
|
boundsData = LatLngBounds(
|
||||||
|
northeast: LatLng(maxLat!, maxLng!),
|
||||||
|
southwest: LatLng(minLat!, minLng!));
|
||||||
|
}
|
||||||
|
|
||||||
|
// تشغيل الأنيميشن الخفيف لومضات المسار + fit camera after
|
||||||
|
_playRouteAnimation(polylineCoordinates, boundsData);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
// 🚨 هذا السطر سيفضح المشكلة الحقيقية! 🚨
|
// 🚨 هذا السطر سيفضح المشكلة الحقيقية! 🚨
|
||||||
print('🚨 CRITICAL ERROR IN getDirectionMap: $e');
|
print('🚨 CRITICAL ERROR IN getDirectionMap: $e');
|
||||||
@@ -6563,25 +6668,28 @@ Intaleq Team''';
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- دالة الأنيميشن الجديدة ---
|
// --- دالة الأنيميشن مع ألوان مختلفة لكل قطعة ---
|
||||||
Future<void> _playRouteAnimation(List<LatLng> coords) async {
|
Future<void> _playRouteAnimation(
|
||||||
const String routeId = 'route_solid';
|
List<LatLng> coords, LatLngBounds? bounds) async {
|
||||||
|
// Segment colors matching UI dots: green → amber → purple → red
|
||||||
|
const List<Color> segmentColors = [
|
||||||
|
Color(0xFF109642), // Green (start → stop 1)
|
||||||
|
Color(0xFFF59E0B), // Amber (stop 1 → stop 2)
|
||||||
|
Color(0xFF7C3AED), // Purple (last segment → dest)
|
||||||
|
Color(0xFFEF4444), // Red (fallback)
|
||||||
|
];
|
||||||
|
|
||||||
// الألوان المطلوبة (بإمكانك تغيير AppColor.primaryColor إلى ما يناسبك)
|
// Loading animation (4 flashes)
|
||||||
Color finalColor =
|
Color lightColor = Colors.grey.shade400;
|
||||||
AppColor.primaryColor; // اللون النهائي الثابت (مثل الأزرق)
|
Color darkColor = Colors.grey.shade700;
|
||||||
Color lightColor = Colors.grey.shade400; // لون التحديث الفاتح (رمادي)
|
|
||||||
Color darkColor = Colors.grey.shade700; // لون التحديث الغامق (رمادي غامق)
|
|
||||||
|
|
||||||
// تكرار العملية 4 مرات لإعطاء تأثير التحميل والتحديث
|
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
// الحالة 1: لون فاتح وعرض أقل
|
polyLines.removeWhere((p) => p.polylineId.value.startsWith('route_'));
|
||||||
polyLines.removeWhere((p) => p.polylineId.value == routeId);
|
|
||||||
polyLines.add(Polyline(
|
polyLines.add(Polyline(
|
||||||
polylineId: const PolylineId(routeId),
|
polylineId: const PolylineId('route_solid'),
|
||||||
points: coords,
|
points: coords,
|
||||||
width: 5,
|
width: 5,
|
||||||
color: lightColor,
|
color: i.isEven ? lightColor : darkColor,
|
||||||
endCap: Cap.roundCap,
|
endCap: Cap.roundCap,
|
||||||
startCap: Cap.roundCap,
|
startCap: Cap.roundCap,
|
||||||
jointType: JointType.round,
|
jointType: JointType.round,
|
||||||
@@ -6589,36 +6697,76 @@ Intaleq Team''';
|
|||||||
));
|
));
|
||||||
update();
|
update();
|
||||||
await Future.delayed(const Duration(milliseconds: 250));
|
await Future.delayed(const Duration(milliseconds: 250));
|
||||||
|
}
|
||||||
|
|
||||||
// الحالة 2: لون غامق وعرض أكبر
|
// After animation: draw coloured segments if we have waypoints
|
||||||
polyLines.removeWhere((p) => p.polylineId.value == routeId);
|
polyLines.removeWhere((p) => p.polylineId.value.startsWith('route_'));
|
||||||
|
|
||||||
|
if (activeMenuWaypointCount > 0) {
|
||||||
|
// Find split indices: closest polyline point to each active waypoint
|
||||||
|
List<int> splitIndices = [];
|
||||||
|
for (int w = 0; w < activeMenuWaypointCount; w++) {
|
||||||
|
final wp = menuWaypoints[w];
|
||||||
|
if (wp == null) continue;
|
||||||
|
int bestIdx = 0;
|
||||||
|
double bestDist = double.infinity;
|
||||||
|
for (int j = 0; j < coords.length; j++) {
|
||||||
|
final dx = coords[j].latitude - wp.latitude;
|
||||||
|
final dy = coords[j].longitude - wp.longitude;
|
||||||
|
final d = dx * dx + dy * dy;
|
||||||
|
if (d < bestDist) {
|
||||||
|
bestDist = d;
|
||||||
|
bestIdx = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
splitIndices.add(bestIdx);
|
||||||
|
}
|
||||||
|
splitIndices.sort();
|
||||||
|
|
||||||
|
// Build segments: [0..split0], [split0..split1], ..., [splitN..end]
|
||||||
|
List<int> boundaries = [0, ...splitIndices, coords.length - 1];
|
||||||
|
for (int s = 0; s < boundaries.length - 1; s++) {
|
||||||
|
int from = boundaries[s];
|
||||||
|
int to = boundaries[s + 1] + 1; // inclusive end
|
||||||
|
if (to > coords.length) to = coords.length;
|
||||||
|
if (from >= to - 1) continue; // skip empty
|
||||||
|
final segCoords = coords.sublist(from, to);
|
||||||
|
if (segCoords.length < 2) continue;
|
||||||
|
final color = segmentColors[s % segmentColors.length];
|
||||||
polyLines.add(Polyline(
|
polyLines.add(Polyline(
|
||||||
polylineId: const PolylineId(routeId),
|
polylineId: PolylineId('route_seg_$s'),
|
||||||
points: coords,
|
points: segCoords,
|
||||||
width: 6,
|
width: 6,
|
||||||
color: darkColor,
|
color: color,
|
||||||
endCap: Cap.roundCap,
|
endCap: Cap.roundCap,
|
||||||
startCap: Cap.roundCap,
|
startCap: Cap.roundCap,
|
||||||
jointType: JointType.round,
|
jointType: JointType.round,
|
||||||
zIndex: 2,
|
zIndex: 3 + s,
|
||||||
));
|
));
|
||||||
update();
|
|
||||||
await Future.delayed(const Duration(milliseconds: 250));
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// بعد الانتهاء من الأنيميشن، يتم تثبيت المسار على اللون الأساسي للتطبيق
|
// Single leg: solid primary color
|
||||||
polyLines.removeWhere((p) => p.polylineId.value == routeId);
|
|
||||||
polyLines.add(Polyline(
|
polyLines.add(Polyline(
|
||||||
polylineId: const PolylineId(routeId),
|
polylineId: const PolylineId('route_solid'),
|
||||||
points: coords,
|
points: coords,
|
||||||
width: 6,
|
width: 6,
|
||||||
color: finalColor,
|
color: AppColor.primaryColor,
|
||||||
endCap: Cap.roundCap,
|
endCap: Cap.roundCap,
|
||||||
startCap: Cap.roundCap,
|
startCap: Cap.roundCap,
|
||||||
jointType: JointType.round,
|
jointType: JointType.round,
|
||||||
zIndex: 3,
|
zIndex: 3,
|
||||||
));
|
));
|
||||||
|
}
|
||||||
update();
|
update();
|
||||||
|
|
||||||
|
// Fit camera to full route bounds AFTER polylines are drawn
|
||||||
|
if (bounds != null) {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
try {
|
||||||
|
mapController
|
||||||
|
?.animateCamera(CameraUpdate.newLatLngBounds(bounds, 100));
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- دالة المساعدة لإعادة المحاولة ---
|
// --- دالة المساعدة لإعادة المحاولة ---
|
||||||
@@ -7341,7 +7489,9 @@ Intaleq Team''';
|
|||||||
final DateTime currentTime = DateTime.now();
|
final DateTime currentTime = DateTime.now();
|
||||||
newTime = currentTime.add(durationToAdd);
|
newTime = currentTime.add(durationToAdd);
|
||||||
averageDuration = (durationToRide / 60) / distance;
|
averageDuration = (durationToRide / 60) / distance;
|
||||||
final int totalMinutes = (durationToRide / 60).floor();
|
// +5 minutes per waypoint stop surcharge
|
||||||
|
final int waypointSurchargeMinutes = activeMenuWaypointCount * 5;
|
||||||
|
final int totalMinutes = (durationToRide / 60).floor() + waypointSurchargeMinutes;
|
||||||
|
|
||||||
// ====== أدوات مساعدة ======
|
// ====== أدوات مساعدة ======
|
||||||
bool _isAirport(String s) {
|
bool _isAirport(String s) {
|
||||||
|
|||||||
@@ -1429,6 +1429,17 @@ class MyTranslation extends Translations {
|
|||||||
"الآن حرك الخريطة إلى نقطة الانطلاق الخاصة بك",
|
"الآن حرك الخريطة إلى نقطة الانطلاق الخاصة بك",
|
||||||
'Move map to your pickup point':
|
'Move map to your pickup point':
|
||||||
"حرك الخريطة إلى نقطة الانطلاق الخاصة بك",
|
"حرك الخريطة إلى نقطة الانطلاق الخاصة بك",
|
||||||
|
'You have a balance of': "لديك رصيد بقيمة",
|
||||||
|
'Negative Balance:': "رصيد سلبي:",
|
||||||
|
'You have a negative balance of': "لديك رصيد سلبي بقيمة",
|
||||||
|
'Have a Promo Code?': "هل لديك رمز عرض؟",
|
||||||
|
'Tap to apply your discount': "اضغط لتطبيق خصمك",
|
||||||
|
'Plan Your Route': "خطط طريقك",
|
||||||
|
'Add a Stop': "إضافة نقطة توقف",
|
||||||
|
'stop(s)': "نقطة توقف",
|
||||||
|
'min added to fare': "دقيقة مضافة للأجرة",
|
||||||
|
'Set as Stop': "تعيين كنقطة توقف",
|
||||||
|
'Move map to set stop': "حرك الخريطة لتعيين نقطة التوقف",
|
||||||
'Move map to set start location': "حرك الخريطة لتعيين موقع الانطلاق",
|
'Move map to set start location': "حرك الخريطة لتعيين موقع الانطلاق",
|
||||||
'Move map to your work location': "حرك الخريطة إلى موقع عملك",
|
'Move map to your work location': "حرك الخريطة إلى موقع عملك",
|
||||||
'Move map to your home location': "حرك الخريطة إلى موقع منزلك",
|
'Move map to your home location': "حرك الخريطة إلى موقع منزلك",
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ import 'dart:ui';
|
|||||||
import '../../../constant/info.dart';
|
import '../../../constant/info.dart';
|
||||||
import '../../../controller/functions/tts.dart';
|
import '../../../controller/functions/tts.dart';
|
||||||
import '../../../controller/home/map_passenger_controller.dart';
|
import '../../../controller/home/map_passenger_controller.dart';
|
||||||
import '../../../print.dart';
|
|
||||||
import '../../widgets/mydialoug.dart';
|
import '../../widgets/mydialoug.dart';
|
||||||
|
|
||||||
// --- CarType class (Unchanged) ---
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// CAR TYPE MODEL
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
class CarType {
|
class CarType {
|
||||||
final String carType;
|
final String carType;
|
||||||
final String carDetail;
|
final String carDetail;
|
||||||
@@ -27,7 +28,6 @@ class CarType {
|
|||||||
{required this.carType, required this.carDetail, required this.image});
|
{required this.carType, required this.carDetail, required this.image});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- List of Car Types (Unchanged) ---
|
|
||||||
List<CarType> carTypes = [
|
List<CarType> carTypes = [
|
||||||
CarType(
|
CarType(
|
||||||
carType: 'Fixed Price',
|
carType: 'Fixed Price',
|
||||||
@@ -55,7 +55,9 @@ List<CarType> carTypes = [
|
|||||||
image: 'assets/images/roundtrip.png'),
|
image: 'assets/images/roundtrip.png'),
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- Main Widget ---
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// MAIN WIDGET
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
class CarDetailsTypeToChoose extends StatelessWidget {
|
class CarDetailsTypeToChoose extends StatelessWidget {
|
||||||
CarDetailsTypeToChoose({super.key});
|
CarDetailsTypeToChoose({super.key});
|
||||||
final textToSpeechController = Get.put(TextToSpeechController());
|
final textToSpeechController = Get.put(TextToSpeechController());
|
||||||
@@ -82,152 +84,214 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main Bottom Sheet Design
|
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(28),
|
||||||
|
topRight: Radius.circular(28),
|
||||||
|
),
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor
|
color: AppColor.secondaryColor.withAlpha(240),
|
||||||
.secondaryColor, // Solid background for better performance
|
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius: const BorderRadius.only(
|
||||||
topLeft: Radius.circular(30),
|
topLeft: Radius.circular(28),
|
||||||
topRight: Radius.circular(30),
|
topRight: Radius.circular(28),
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.15),
|
color: Colors.black.withAlpha(30),
|
||||||
blurRadius: 20,
|
blurRadius: 30,
|
||||||
spreadRadius: 5,
|
spreadRadius: 0,
|
||||||
offset: const Offset(0, -5),
|
offset: const Offset(0, -8),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Drag Handle
|
// ── Drag Handle ──────────────────────────────────────
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 50,
|
width: 40,
|
||||||
height: 5,
|
height: 4,
|
||||||
margin: const EdgeInsets.symmetric(vertical: 12),
|
margin: const EdgeInsets.only(top: 12, bottom: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey.withOpacity(0.3),
|
color: Colors.grey.shade300,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Header (Title + Trip Info)
|
// ── Header ───────────────────────────────────────────
|
||||||
_buildModernHeader(controller),
|
_buildHeader(controller),
|
||||||
|
|
||||||
// Warning Message (if any)
|
// ── Negative Balance Warning ─────────────────────────
|
||||||
_buildNegativeBalanceWarning(controller),
|
_buildNegativeBalanceWarning(controller),
|
||||||
|
|
||||||
// Car List
|
// ── Car Selection List ───────────────────────────────
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 165, // Fixed height for consistency
|
height: 170,
|
||||||
child: ListView.separated(
|
child: ListView.builder(
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding:
|
padding: const EdgeInsets.fromLTRB(20, 8, 20, 12),
|
||||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
|
||||||
itemCount: carTypes.length,
|
itemCount: carTypes.length,
|
||||||
separatorBuilder: (context, index) =>
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final carType = carTypes[index];
|
final carType = carTypes[index];
|
||||||
final isSelected = controller.selectedIndex == index;
|
final isSelected = controller.selectedIndex == index;
|
||||||
return _buildVerticalCarCard(
|
return Padding(
|
||||||
context, controller, carType, isSelected, index);
|
padding: EdgeInsets.only(
|
||||||
|
right: index < carTypes.length - 1 ? 10 : 0),
|
||||||
|
child: _buildCarCard(
|
||||||
|
context, controller, carType, isSelected, index),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Promo Code Button
|
// ── Promo Code & Actions ─────────────────────────────
|
||||||
_buildPromoButton(context, controller),
|
_buildPromoButton(context, controller),
|
||||||
|
|
||||||
// Safe Area spacing
|
SizedBox(
|
||||||
SizedBox(height: MediaQuery.of(context).padding.bottom + 10),
|
height: MediaQuery.of(context).padding.bottom + 10),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- UI Components ---
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// HEADER
|
||||||
Widget _buildModernHeader(MapPassengerController controller) {
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
Widget _buildHeader(MapPassengerController controller) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 5),
|
padding: const EdgeInsets.fromLTRB(22, 4, 22, 8),
|
||||||
child: Row(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Title
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Choose your ride'.tr,
|
'Choose your ride'.tr,
|
||||||
style: AppStyle.headTitle.copyWith(
|
style: AppStyle.headTitle.copyWith(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
letterSpacing: 0.5),
|
letterSpacing: 0.3,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
// Trip Info Pill
|
// Close button
|
||||||
Container(
|
GestureDetector(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
onTap: () {
|
||||||
|
controller.isBottomSheetShown = false;
|
||||||
|
controller.update();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.primaryColor.withOpacity(0.08),
|
color: Colors.grey.shade100,
|
||||||
borderRadius: BorderRadius.circular(20),
|
shape: BoxShape.circle,
|
||||||
border: Border.all(color: AppColor.primaryColor.withOpacity(0.2)),
|
),
|
||||||
|
child: Icon(Icons.close_rounded,
|
||||||
|
size: 18, color: Colors.grey.shade600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
// Trip Stats Row
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Distance chip
|
||||||
|
_buildStatChip(
|
||||||
|
icon: Icons.route_rounded,
|
||||||
|
value:
|
||||||
|
'${controller.distance.toStringAsFixed(1)} ${'KM'.tr}',
|
||||||
|
color: AppColor.primaryColor,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
// Duration chip
|
||||||
|
_buildStatChip(
|
||||||
|
icon: Icons.schedule_rounded,
|
||||||
|
value: controller.hours > 0
|
||||||
|
? '${controller.hours}h ${controller.minutes}m'
|
||||||
|
: '${controller.minutes} ${'min'.tr}',
|
||||||
|
color: const Color(0xFF6366F1), // Indigo
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
// Price preview for selected car
|
||||||
|
if (controller.selectedIndex >= 0 &&
|
||||||
|
controller.selectedIndex < carTypes.length)
|
||||||
|
Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
AppColor.primaryColor,
|
||||||
|
AppColor.primaryColor.withAlpha(200),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${_getPassengerPriceText(carTypes[controller.selectedIndex], controller)} ${'SYP'.tr}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.directions_car_filled_outlined,
|
|
||||||
size: 16, color: AppColor.primaryColor),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
'${controller.distance.toStringAsFixed(1)} ${'KM'.tr}',
|
|
||||||
style: AppStyle.subtitle.copyWith(
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColor.primaryColor),
|
|
||||||
),
|
),
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
height: 12,
|
|
||||||
width: 1,
|
|
||||||
color: Colors.grey.shade400),
|
|
||||||
Icon(Icons.access_time_filled_rounded,
|
|
||||||
size: 16, color: AppColor.primaryColor),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
controller.hours > 0
|
|
||||||
? '${controller.hours}h ${controller.minutes}m'
|
|
||||||
: '${controller.minutes} min',
|
|
||||||
style: AppStyle.subtitle.copyWith(
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColor.primaryColor),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildVerticalCarCard(
|
Widget _buildStatChip(
|
||||||
BuildContext context,
|
{required IconData icon, required String value, required Color color}) {
|
||||||
MapPassengerController controller,
|
return Container(
|
||||||
CarType carType,
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
bool isSelected,
|
decoration: BoxDecoration(
|
||||||
int index) {
|
color: color.withAlpha(20),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: color.withAlpha(50)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 14, color: color),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// CAR CARD
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
Widget _buildCarCard(BuildContext context, MapPassengerController controller,
|
||||||
|
CarType carType, bool isSelected, int index) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
controller.selectCarFromList(index);
|
controller.selectCarFromList(index);
|
||||||
@@ -235,67 +299,97 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
context, controller, carType, textToSpeechController);
|
context, controller, carType, textToSpeechController);
|
||||||
},
|
},
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 300),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeOutCubic,
|
||||||
width: 110,
|
width: 108,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
gradient: isSelected
|
||||||
? AppColor.primaryColor.withOpacity(0.05)
|
? LinearGradient(
|
||||||
: Colors.white,
|
begin: Alignment.topLeft,
|
||||||
borderRadius: BorderRadius.circular(18),
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
AppColor.primaryColor.withAlpha(18),
|
||||||
|
AppColor.primaryColor.withAlpha(8),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
color: isSelected ? null : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isSelected
|
color:
|
||||||
? AppColor.primaryColor
|
isSelected ? AppColor.primaryColor : Colors.grey.shade200,
|
||||||
: Colors.grey.withOpacity(0.2),
|
|
||||||
width: isSelected ? 2.0 : 1.0,
|
width: isSelected ? 2.0 : 1.0,
|
||||||
),
|
),
|
||||||
boxShadow: isSelected
|
boxShadow: [
|
||||||
? [
|
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: AppColor.primaryColor.withOpacity(0.2),
|
color: isSelected
|
||||||
blurRadius: 8,
|
? AppColor.primaryColor.withAlpha(40)
|
||||||
offset: const Offset(0, 4),
|
: Colors.black.withAlpha(12),
|
||||||
)
|
blurRadius: isSelected ? 12 : 4,
|
||||||
]
|
offset: const Offset(0, 3),
|
||||||
: [
|
),
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.1),
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
|
// Selected indicator
|
||||||
|
if (isSelected)
|
||||||
|
Positioned(
|
||||||
|
top: 6,
|
||||||
|
right: 6,
|
||||||
|
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: 6,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child:
|
||||||
|
const Icon(Icons.check, size: 11, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Card content
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.fromLTRB(8, 10, 8, 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Image with subtle scaling if selected
|
// Car image
|
||||||
AnimatedScale(
|
AnimatedScale(
|
||||||
scale: isSelected ? 1.1 : 1.0,
|
scale: isSelected ? 1.12 : 1.0,
|
||||||
duration: const Duration(milliseconds: 250),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
carType.image,
|
carType.image,
|
||||||
height: 50,
|
height: 48,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
// Car Type Text
|
// Car name
|
||||||
FittedBox(
|
FittedBox(
|
||||||
fit: BoxFit.scaleDown,
|
fit: BoxFit.scaleDown,
|
||||||
child: Text(
|
child: Text(
|
||||||
carType.carType.tr,
|
carType.carType.tr,
|
||||||
style: AppStyle.subtitle.copyWith(
|
style: TextStyle(
|
||||||
fontWeight:
|
fontWeight:
|
||||||
isSelected ? FontWeight.w800 : FontWeight.w600,
|
isSelected ? FontWeight.w800 : FontWeight.w600,
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
color:
|
color: isSelected
|
||||||
isSelected ? AppColor.primaryColor : Colors.black87,
|
? AppColor.primaryColor
|
||||||
|
: Colors.grey.shade800,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
@@ -303,23 +397,27 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
// Price Tag
|
// Price tag
|
||||||
Container(
|
Container(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(
|
||||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? AppColor.primaryColor
|
? AppColor.primaryColor
|
||||||
: Colors.grey.shade100,
|
: Colors.grey.shade50,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: isSelected
|
||||||
|
? null
|
||||||
|
: Border.all(color: Colors.grey.shade200),
|
||||||
),
|
),
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
child: Text(
|
child: Text(
|
||||||
'${_getPassengerPriceText(carType, controller)} ${'SYP'.tr}',
|
'${_getPassengerPriceText(carType, controller)} ${'SYP'.tr}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w700,
|
||||||
color: isSelected ? Colors.white : Colors.black87,
|
color:
|
||||||
|
isSelected ? Colors.white : Colors.grey.shade700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -327,75 +425,80 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Checkmark Badge for Selected Item
|
|
||||||
if (isSelected)
|
|
||||||
Positioned(
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(2),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: AppColor.primaryColor,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.check, size: 12, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// PROMO BUTTON
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
Widget _buildPromoButton(
|
Widget _buildPromoButton(
|
||||||
BuildContext context, MapPassengerController controller) {
|
BuildContext context, MapPassengerController controller) {
|
||||||
if (controller.promoTaken) return const SizedBox.shrink();
|
if (controller.promoTaken) return const SizedBox.shrink();
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 5),
|
padding: const EdgeInsets.fromLTRB(20, 6, 20, 4),
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => _showPromoCodeDialog(context, controller),
|
onTap: () => _showPromoCodeDialog(context, controller),
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 12, horizontal: 14),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey.withOpacity(0.05),
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.amber.shade50,
|
||||||
|
Colors.orange.shade50,
|
||||||
|
],
|
||||||
|
),
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
border: Border.all(color: Colors.amber.shade200),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(7),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.primaryColor.withOpacity(0.1),
|
color: Colors.amber.shade100,
|
||||||
shape: BoxShape.circle),
|
shape: BoxShape.circle,
|
||||||
child: Icon(Icons.confirmation_number_outlined,
|
|
||||||
color: AppColor.primaryColor, size: 20),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 14),
|
child: Icon(Icons.percent_rounded,
|
||||||
|
color: Colors.amber.shade800, size: 16),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Promo Code'.tr,
|
'Have a Promo Code?'.tr,
|
||||||
style: AppStyle.subtitle.copyWith(
|
style: TextStyle(
|
||||||
fontSize: 14, fontWeight: FontWeight.bold),
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.amber.shade900,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Have a promo code?'.tr,
|
'Tap to apply your discount'.tr,
|
||||||
style: AppStyle.subtitle.copyWith(
|
style: TextStyle(
|
||||||
fontSize: 12, color: Colors.grey.shade600),
|
fontSize: 11, color: Colors.amber.shade700),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Icon(Icons.arrow_forward_ios_rounded,
|
Container(
|
||||||
size: 16, color: Colors.grey.shade400)
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.amber.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(Icons.arrow_forward_ios_rounded,
|
||||||
|
size: 12, color: Colors.amber.shade800),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -404,30 +507,41 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// NEGATIVE BALANCE WARNING
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
Widget _buildNegativeBalanceWarning(MapPassengerController controller) {
|
Widget _buildNegativeBalanceWarning(MapPassengerController controller) {
|
||||||
final passengerWallet =
|
final passengerWallet =
|
||||||
double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ?? 0.0;
|
double.tryParse(box.read(BoxName.passengerWalletTotal) ?? '0.0') ?? 0.0;
|
||||||
if (passengerWallet < 0.0) {
|
if (passengerWallet < 0.0) {
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 6),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.redColor.withOpacity(0.1),
|
color: Colors.red.shade50,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: AppColor.redColor.withOpacity(0.3)),
|
border: Border.all(color: Colors.red.shade200),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.info_outline_rounded,
|
Container(
|
||||||
color: AppColor.redColor, size: 24),
|
padding: const EdgeInsets.all(6),
|
||||||
const SizedBox(width: 12),
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.shade100,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(Icons.warning_amber_rounded,
|
||||||
|
color: Colors.red.shade700, size: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'${'You have a negative balance of'.tr} ${passengerWallet.toStringAsFixed(2)} ${'SYP'.tr}.',
|
'${'You have a negative balance of'.tr} ${passengerWallet.toStringAsFixed(2)} ${'SYP'.tr}.',
|
||||||
style: AppStyle.subtitle.copyWith(
|
style: TextStyle(
|
||||||
color: AppColor.redColor,
|
color: Colors.red.shade800,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 13),
|
fontSize: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -437,8 +551,9 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Logic Helpers (Copied from your previous code to ensure functionality) ---
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// PRICING HELPERS (Unchanged logic)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
String _getPassengerPriceText(
|
String _getPassengerPriceText(
|
||||||
CarType carType, MapPassengerController mapPassengerController) {
|
CarType carType, MapPassengerController mapPassengerController) {
|
||||||
double rawPrice;
|
double rawPrice;
|
||||||
@@ -476,8 +591,9 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
return formatter.format(roundedPrice);
|
return formatter.format(roundedPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Dialogs (Styled consistently) ---
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// DIALOGS
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
void _showPromoCodeDialog(
|
void _showPromoCodeDialog(
|
||||||
BuildContext context, MapPassengerController controller) {
|
BuildContext context, MapPassengerController controller) {
|
||||||
Get.dialog(
|
Get.dialog(
|
||||||
@@ -548,7 +664,7 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
barrierColor: Colors.black.withOpacity(0.5),
|
barrierColor: Colors.black.withAlpha(130),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,11 +683,18 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.only(top: 60),
|
margin: const EdgeInsets.only(top: 55),
|
||||||
padding: const EdgeInsets.fromLTRB(24, 70, 24, 24),
|
padding: const EdgeInsets.fromLTRB(24, 65, 24, 20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.secondaryColor,
|
color: AppColor.secondaryColor,
|
||||||
borderRadius: BorderRadius.circular(28.0),
|
borderRadius: BorderRadius.circular(28.0),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withAlpha(30),
|
||||||
|
blurRadius: 30,
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -581,7 +704,10 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
carType.carType.tr,
|
carType.carType.tr,
|
||||||
style: AppStyle.headTitle.copyWith(fontSize: 22),
|
style: AppStyle.headTitle.copyWith(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
InkWell(
|
InkWell(
|
||||||
@@ -589,38 +715,75 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
_getCarDescription(
|
_getCarDescription(
|
||||||
mapPassengerController, carType)),
|
mapPassengerController, carType)),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Padding(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(4.0),
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColor.primaryColor.withAlpha(20),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
child: Icon(Icons.volume_up_rounded,
|
child: Icon(Icons.volume_up_rounded,
|
||||||
color: AppColor.primaryColor, size: 24),
|
color: AppColor.primaryColor, size: 20),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 6),
|
||||||
|
// Price badge in dialog
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 14, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey.withOpacity(0.05),
|
gradient: LinearGradient(
|
||||||
borderRadius: BorderRadius.circular(16)),
|
colors: [
|
||||||
|
AppColor.primaryColor,
|
||||||
|
AppColor.primaryColor.withAlpha(200),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${_getPassengerPriceText(carType, mapPassengerController)} ${'SYP'.tr}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
border: Border.all(color: Colors.grey.shade100),
|
||||||
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
_getCarDescription(mapPassengerController, carType),
|
_getCarDescription(mapPassengerController, carType),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: AppStyle.subtitle.copyWith(
|
style: AppStyle.subtitle.copyWith(
|
||||||
color: Colors.black87,
|
color: Colors.grey.shade700,
|
||||||
fontSize: 15,
|
fontSize: 14,
|
||||||
height: 1.4,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 20),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () => Get.back(),
|
onPressed: () => Get.back(),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
side: BorderSide(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
),
|
||||||
child: Text('Back'.tr,
|
child: Text('Back'.tr,
|
||||||
style: TextStyle(color: Colors.grey.shade600)),
|
style:
|
||||||
|
TextStyle(color: Colors.grey.shade600)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@@ -641,22 +804,38 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Floating car image
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withAlpha(20),
|
||||||
|
blurRadius: 15,
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: 'car_${carType.carType}',
|
tag: 'car_${carType.carType}',
|
||||||
child: Image.asset(carType.image, height: 130),
|
child: Image.asset(carType.image, height: 80),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
barrierColor: Colors.black.withOpacity(0.6),
|
barrierColor: Colors.black.withAlpha(150),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Logic Helpers (Keep unchanged) ---
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// LOGIC HELPERS (Unchanged)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
String _getCarDescription(
|
String _getCarDescription(
|
||||||
MapPassengerController mapPassengerController, CarType carType) {
|
MapPassengerController mapPassengerController, CarType carType) {
|
||||||
switch (carType.carType) {
|
switch (carType.carType) {
|
||||||
@@ -787,7 +966,9 @@ class CarDetailsTypeToChoose extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- BurcMoney Widget (Unchanged) ---
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// BURC MONEY WIDGET (Floating negative balance banner)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
class BurcMoney extends StatelessWidget {
|
class BurcMoney extends StatelessWidget {
|
||||||
const BurcMoney({super.key});
|
const BurcMoney({super.key});
|
||||||
|
|
||||||
@@ -810,13 +991,13 @@ class BurcMoney extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColor.redColor.withOpacity(0.8),
|
color: AppColor.redColor.withAlpha(220),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(14),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.1),
|
color: Colors.red.withAlpha(40),
|
||||||
blurRadius: 4,
|
blurRadius: 12,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -191,20 +191,19 @@ class _ExpandedView extends StatelessWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// ── هيدر "Quick Actions" ─────────────────────────────────────────
|
// ── Header ────────────────────────────────────────────────────────
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(18, 14, 12, 8),
|
padding: const EdgeInsets.fromLTRB(18, 14, 12, 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Quick Actions'.tr,
|
'Plan Your Route'.tr,
|
||||||
style: AppStyle.title.copyWith(
|
style: AppStyle.title.copyWith(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
// زر إغلاق
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: controller.changeMainBottomMenuMap,
|
onTap: controller.changeMainBottomMenuMap,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -222,36 +221,236 @@ class _ExpandedView extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
|
|
||||||
const Divider(height: 1, thickness: 0.5),
|
const Divider(height: 1, thickness: 0.5),
|
||||||
const SizedBox(height: 10),
|
|
||||||
|
|
||||||
// ── موقع البداية ─────────────────────────────────────────────────
|
|
||||||
if (!controller.isAnotherOreder)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: _LocationRow(
|
|
||||||
icon: Icons.my_location_rounded,
|
|
||||||
iconColor: AppColor.primaryColor,
|
|
||||||
label: controller.currentLocationString,
|
|
||||||
isStart: true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: formSearchPlacesStart(),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
|
|
||||||
// ── حقل الوجهة ────────────────────────────────────────────────────
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: formSearchPlacesDestenation(),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// ── زر WhatsApp ───────────────────────────────────────────────────
|
// ── Route Timeline (no IntrinsicHeight) ─────────────────────────
|
||||||
|
// Start location row with green dot
|
||||||
|
_buildTimelineItem(
|
||||||
|
dotColor: AppColor.primaryColor,
|
||||||
|
showTopLine: false,
|
||||||
|
showBottomLine: true,
|
||||||
|
child: !controller.isAnotherOreder
|
||||||
|
? _TimelineRow(
|
||||||
|
icon: Icons.my_location_rounded,
|
||||||
|
iconColor: AppColor.primaryColor,
|
||||||
|
bgColor: AppColor.primaryColor,
|
||||||
|
label: controller.currentLocationString,
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16),
|
||||||
|
child: formSearchPlacesStart(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Waypoint stop rows
|
||||||
|
...List.generate(controller.activeMenuWaypointCount, (index) {
|
||||||
|
final wpName = controller.menuWaypointNames[index];
|
||||||
|
final isSet = controller.menuWaypoints[index] != null;
|
||||||
|
// Stop 1 = amber/orange, Stop 2 = deep purple
|
||||||
|
final Color dotColor =
|
||||||
|
index == 0 ? Colors.amber.shade700 : Colors.deepPurple.shade400;
|
||||||
|
return _buildTimelineItem(
|
||||||
|
dotColor: dotColor,
|
||||||
|
showTopLine: true,
|
||||||
|
showBottomLine: true,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: index == 0
|
||||||
|
? Colors.amber.shade50
|
||||||
|
: Colors.deepPurple.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(
|
||||||
|
color: isSet
|
||||||
|
? (index == 0
|
||||||
|
? Colors.amber.shade300
|
||||||
|
: Colors.deepPurple.shade200)
|
||||||
|
: Colors.grey.shade200,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Numbered badge
|
||||||
|
Container(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: index == 0
|
||||||
|
? [Colors.amber.shade500, Colors.amber.shade700]
|
||||||
|
: [
|
||||||
|
Colors.deepPurple.shade300,
|
||||||
|
Colors.deepPurple.shade500
|
||||||
|
],
|
||||||
|
),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'${index + 1}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
controller.changeMainBottomMenuMap();
|
||||||
|
controller.startPickingWaypointOnMap(index);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
isSet ? wpName : '${'Stop'.tr} ${index + 1}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: isSet
|
||||||
|
? (index == 0
|
||||||
|
? Colors.amber.shade900
|
||||||
|
: Colors.deepPurple.shade700)
|
||||||
|
: Colors.grey.shade400,
|
||||||
|
fontWeight: isSet ? FontWeight.w500 : FontWeight.w400,
|
||||||
|
fontStyle:
|
||||||
|
isSet ? FontStyle.normal : FontStyle.italic,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
controller.changeMainBottomMenuMap();
|
||||||
|
controller.startPickingWaypointOnMap(index);
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
child: Icon(Icons.map_outlined,
|
||||||
|
color: index == 0
|
||||||
|
? Colors.amber.shade600
|
||||||
|
: Colors.deepPurple.shade400,
|
||||||
|
size: 18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => controller.removeMenuWaypoint(index),
|
||||||
|
child: Container(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.red.shade50,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(Icons.close_rounded,
|
||||||
|
color: Colors.red.shade400, size: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Add Stop button
|
||||||
|
if (controller.activeMenuWaypointCount < 2)
|
||||||
|
_buildTimelineItem(
|
||||||
|
dotColor: Colors.grey.shade300,
|
||||||
|
isDotDashed: true,
|
||||||
|
showTopLine: true,
|
||||||
|
showBottomLine: true,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => controller.addMenuWaypoint(),
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: Colors.orange.shade200),
|
||||||
|
color: Colors.orange.shade50.withAlpha(100),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.add_circle_outline_rounded,
|
||||||
|
color: Colors.orange.shade500, size: 16),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'Add a Stop'.tr,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.orange.shade700,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 5, vertical: 1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'+5 ${'min'.tr}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.orange.shade700,
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Destination row with red dot
|
||||||
|
_buildTimelineItem(
|
||||||
|
dotColor: Colors.red.shade500,
|
||||||
|
showTopLine: true,
|
||||||
|
showBottomLine: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16),
|
||||||
|
child: formSearchPlacesDestenation(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// ── Surcharge info ───────────────────────────────────────────────
|
||||||
|
if (controller.activeMenuWaypointCount > 0)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 6),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.schedule_rounded,
|
||||||
|
size: 14, color: Colors.orange.shade600),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'${controller.activeMenuWaypointCount} ${'stop(s)'.tr} · +${controller.activeMenuWaypointCount * 5} ${'min added to fare'.tr}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.orange.shade700,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
// ── WhatsApp ─────────────────────────────────────────────────────
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: _WhatsAppLinkButton(controller: controller),
|
child: _WhatsAppLinkButton(controller: controller),
|
||||||
@@ -259,7 +458,7 @@ class _ExpandedView extends StatelessWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
// ── Order for someone else ────────────────────────────────────────
|
// ── Order type ───────────────────────────────────────────────────
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: _OrderTypeButton(controller: controller),
|
child: _OrderTypeButton(controller: controller),
|
||||||
@@ -269,6 +468,59 @@ class _ExpandedView extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a single timeline row: [dot + line] | [child widget]
|
||||||
|
Widget _buildTimelineItem({
|
||||||
|
required Color dotColor,
|
||||||
|
required bool showTopLine,
|
||||||
|
required bool showBottomLine,
|
||||||
|
required Widget child,
|
||||||
|
bool isDotDashed = false,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Timeline indicator column (dot + lines)
|
||||||
|
SizedBox(
|
||||||
|
width: 24,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Top connecting line
|
||||||
|
if (showTopLine)
|
||||||
|
Container(
|
||||||
|
width: 2,
|
||||||
|
height: 8,
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
),
|
||||||
|
// Dot
|
||||||
|
Container(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDotDashed ? Colors.transparent : dotColor,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: dotColor, width: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Bottom connecting line
|
||||||
|
if (showBottomLine)
|
||||||
|
Container(
|
||||||
|
width: 2,
|
||||||
|
height: 8,
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
// Content
|
||||||
|
Expanded(child: child),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -282,6 +534,10 @@ class _MapPickerOverlay extends StatelessWidget {
|
|||||||
|
|
||||||
// ── الحصول على نص الحالة الحالية ─────────────────────────────────────────
|
// ── الحصول على نص الحالة الحالية ─────────────────────────────────────────
|
||||||
String _getModeTitle(BuildContext context) {
|
String _getModeTitle(BuildContext context) {
|
||||||
|
if (controller.isPickingWaypoint) {
|
||||||
|
return 'Move map to set stop'.tr +
|
||||||
|
' ${controller.pickingWaypointIndex + 1}'.tr;
|
||||||
|
}
|
||||||
if (controller.passengerStartLocationFromMap) {
|
if (controller.passengerStartLocationFromMap) {
|
||||||
return 'Move map to your pickup point'.tr;
|
return 'Move map to your pickup point'.tr;
|
||||||
} else if (controller.startLocationFromMap) {
|
} else if (controller.startLocationFromMap) {
|
||||||
@@ -295,6 +551,9 @@ class _MapPickerOverlay extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _getConfirmLabel(BuildContext context) {
|
String _getConfirmLabel(BuildContext context) {
|
||||||
|
if (controller.isPickingWaypoint) {
|
||||||
|
return 'Set as Stop'.tr;
|
||||||
|
}
|
||||||
if (controller.passengerStartLocationFromMap) {
|
if (controller.passengerStartLocationFromMap) {
|
||||||
return 'Confirm Pickup Location'.tr;
|
return 'Confirm Pickup Location'.tr;
|
||||||
} else if (controller.workLocationFromMap) {
|
} else if (controller.workLocationFromMap) {
|
||||||
@@ -306,6 +565,7 @@ class _MapPickerOverlay extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
IconData _getModeIcon() {
|
IconData _getModeIcon() {
|
||||||
|
if (controller.isPickingWaypoint) return Icons.add_location_alt_rounded;
|
||||||
if (controller.passengerStartLocationFromMap)
|
if (controller.passengerStartLocationFromMap)
|
||||||
return Icons.person_pin_circle_rounded;
|
return Icons.person_pin_circle_rounded;
|
||||||
if (controller.workLocationFromMap) return Icons.work_rounded;
|
if (controller.workLocationFromMap) return Icons.work_rounded;
|
||||||
@@ -314,6 +574,7 @@ class _MapPickerOverlay extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Color _getModeColor() {
|
Color _getModeColor() {
|
||||||
|
if (controller.isPickingWaypoint) return Colors.orange.shade600;
|
||||||
if (controller.passengerStartLocationFromMap) return Colors.green.shade600;
|
if (controller.passengerStartLocationFromMap) return Colors.green.shade600;
|
||||||
if (controller.workLocationFromMap) return Colors.blue.shade600;
|
if (controller.workLocationFromMap) return Colors.blue.shade600;
|
||||||
if (controller.homeLocationFromMap) return Colors.orange.shade600;
|
if (controller.homeLocationFromMap) return Colors.orange.shade600;
|
||||||
@@ -428,6 +689,8 @@ class _MapPickerOverlay extends StatelessWidget {
|
|||||||
controller.startLocationFromMap = false;
|
controller.startLocationFromMap = false;
|
||||||
controller.workLocationFromMap = false;
|
controller.workLocationFromMap = false;
|
||||||
controller.homeLocationFromMap = false;
|
controller.homeLocationFromMap = false;
|
||||||
|
controller.isPickingWaypoint = false;
|
||||||
|
controller.pickingWaypointIndex = -1;
|
||||||
// أعد الخريطة لحالتها المنهارة
|
// أعد الخريطة لحالتها المنهارة
|
||||||
if (!controller.isMainBottomMenuMap) {
|
if (!controller.isMainBottomMenuMap) {
|
||||||
controller.isMainBottomMenuMap = true;
|
controller.isMainBottomMenuMap = true;
|
||||||
@@ -498,6 +761,24 @@ class _MapPickerOverlay extends StatelessWidget {
|
|||||||
controller.newMyLocation.longitude,
|
controller.newMyLocation.longitude,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ── CASE 0: Waypoint picker mode ──────────────────────────────────────
|
||||||
|
if (controller.isPickingWaypoint && controller.pickingWaypointIndex >= 0) {
|
||||||
|
final int wpIndex = controller.pickingWaypointIndex;
|
||||||
|
controller.setMenuWaypointFromMap(wpIndex, currentCameraPosition);
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Stop ${wpIndex + 1} Set'.tr,
|
||||||
|
'Waypoint has been set successfully'.tr,
|
||||||
|
backgroundColor: Colors.orange.shade600,
|
||||||
|
colorText: Colors.white,
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
margin: const EdgeInsets.all(12),
|
||||||
|
borderRadius: 12,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
controller.clearPolyline();
|
controller.clearPolyline();
|
||||||
controller.data = [];
|
controller.data = [];
|
||||||
|
|
||||||
@@ -593,9 +874,18 @@ class _MapPickerOverlay extends StatelessWidget {
|
|||||||
controller.update();
|
controller.update();
|
||||||
|
|
||||||
// ─── 3. العمليات الـ async بعد حفظ الوجهة بأمان ──────────────────────
|
// ─── 3. العمليات الـ async بعد حفظ الوجهة بأمان ──────────────────────
|
||||||
if (!controller.isAnotherOreder) {
|
try {
|
||||||
// تحريك الكاميرا لموقع الراكب ليختار منه نقطة الانطلاق
|
if (controller.isAnotherOreder) {
|
||||||
// ملاحظة: هذا يُغير newMyLocation - لكن myDestination محفوظ بأمان ✅
|
// ✅ "Order for someone else": move camera to the OTHER person's
|
||||||
|
// start location (set via the start search form)
|
||||||
|
await controller.mapController?.animateCamera(
|
||||||
|
CameraUpdate.newLatLng(LatLng(
|
||||||
|
controller.newStartPointLocation.latitude,
|
||||||
|
controller.newStartPointLocation.longitude,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Normal flow: move camera to the passenger's own location
|
||||||
await controller.mapController?.animateCamera(
|
await controller.mapController?.animateCamera(
|
||||||
CameraUpdate.newLatLng(LatLng(
|
CameraUpdate.newLatLng(LatLng(
|
||||||
controller.passengerLocation.latitude,
|
controller.passengerLocation.latitude,
|
||||||
@@ -603,11 +893,16 @@ class _MapPickerOverlay extends StatelessWidget {
|
|||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Guard against disposed GoogleMapController
|
||||||
|
}
|
||||||
|
|
||||||
// ─── 4. إشعار المستخدم ──────────────────────────────────────────────
|
// ─── 4. إشعار المستخدم ──────────────────────────────────────────────
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Destination Set'.tr,
|
'Destination Set'.tr,
|
||||||
'Now move the map to your pickup point'.tr,
|
controller.isAnotherOreder
|
||||||
|
? 'Now set the pickup point for the other person'.tr
|
||||||
|
: 'Now move the map to your pickup point'.tr,
|
||||||
backgroundColor: Colors.green.shade600,
|
backgroundColor: Colors.green.shade600,
|
||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
snackPosition: SnackPosition.TOP,
|
snackPosition: SnackPosition.TOP,
|
||||||
@@ -664,6 +959,46 @@ class _LocationRow extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// صف في التايملاين (للعرض داخل عمود المسار)
|
||||||
|
class _TimelineRow extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final Color iconColor;
|
||||||
|
final Color bgColor;
|
||||||
|
final String label;
|
||||||
|
const _TimelineRow({
|
||||||
|
required this.icon,
|
||||||
|
required this.iconColor,
|
||||||
|
required this.bgColor,
|
||||||
|
required this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor.withAlpha(15),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: bgColor.withAlpha(40)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: iconColor, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: AppStyle.subtitle.copyWith(fontSize: 12.5),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// شريحة الأماكن الأخيرة
|
/// شريحة الأماكن الأخيرة
|
||||||
class _RecentPlaceChip extends StatelessWidget {
|
class _RecentPlaceChip extends StatelessWidget {
|
||||||
final MapPassengerController controller;
|
final MapPassengerController controller;
|
||||||
|
|||||||
Reference in New Issue
Block a user