Update: 2026-07-10 16:09:37

This commit is contained in:
Hamza-Ayed
2026-07-10 16:09:37 +03:00
parent 5fb2c25504
commit 81d4715664
20 changed files with 2442 additions and 219 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

@@ -92,7 +92,7 @@ class MapEngineController extends GetxController {
update();
}
void onStyleLoaded() async {
Future<void> onStyleLoaded() async {
Log.print('🗺️ Siro Map Style Loaded. Initializing...');
isStyleLoaded = true;
await _loadMapIcons();
@@ -207,7 +207,12 @@ class MapEngineController extends GetxController {
}
int? _getImageSize(String id) {
if (id == carIcon || id == motoIcon || id == ladyIcon) return 120;
if (id == carIcon || id == motoIcon || id == ladyIcon) {
// Scale based on pixel ratio so it's not tiny on iOS Retina displays
double ratio = ui.window.devicePixelRatio;
if (ratio == 0) ratio = 3.0; // fallback just in case
return (120 * ratio).toInt();
}
return null;
}
@@ -3719,18 +3719,18 @@ class RideLifecycleController extends GetxController {
// بناء الـ markers أولاً ثم وضعها معاً مع polylines في تحديث واحد
final Set<Marker> newMarkers = {
// لا نمرر InfoWindow هنا: صور A/B تحتوي الحرف أصلاً، وأي textField
// إضافي يرسم حرفاً مكرراً فوق الدبوس بعد إصلاح الخطوط في الـ SDK.
Marker(
markerId: const MarkerId('start'),
position: startLoc,
icon: InlqBitmap.fromAsset('assets/images/A.png'),
infoWindow: const InfoWindow(title: 'A'),
icon: InlqBitmap.fromStyleImage('start_icon'),
anchor: const Offset(0.5, 1.0),
),
Marker(
markerId: const MarkerId('end'),
position: endLoc,
icon: InlqBitmap.fromAsset('assets/images/b.png'),
infoWindow: const InfoWindow(title: 'B'),
icon: InlqBitmap.fromStyleImage('end_icon'),
anchor: const Offset(0.5, 1.0),
),
};
@@ -3742,11 +3742,9 @@ class RideLifecycleController extends GetxController {
newMarkers.add(Marker(
markerId: MarkerId('waypoint_$i'),
position: wp,
icon: InlqBitmap.fromAsset(isFirstWaypoint
? 'assets/images/A.png'
: 'assets/images/b.png'),
infoWindow:
InfoWindow(title: isFirstWaypoint ? 'Stop 1' : 'Stop 2'),
icon: InlqBitmap.fromStyleImage(isFirstWaypoint
? 'start_icon'
: 'end_icon'),
anchor: const Offset(0.5, 1.0),
));
}
@@ -3758,27 +3756,12 @@ class RideLifecycleController extends GetxController {
Log.print(
'✅ FIX P1 v2: ${newMarkers.length} markers placed — start: $startLoc, end: $endLoc');
// إضافة الـ markers مباشرة عبر الـ controller لضمان ظهورها
try {
final ctrl = mapEngine.mapController;
if (ctrl != null) {
for (final m in newMarkers) {
await ctrl.addMarker(m);
}
Log.print(
'✅ Added ${newMarkers.length} markers via controller.addMarker()');
} else {
Log.print('⚠️ mapController is null, relying on declarative markers');
}
} catch (e) {
Log.print('⚠️ addMarker via controller failed: $e');
}
await bottomSheet();
await mapEngine.playRouteAnimation(
mapEngine.polylineCoordinates, mapEngine.lastComputedBounds);
mapEngine.update(); // Make sure the map UI rebuilds with the new markers
update();
} catch (e, stackTrace) {
if (isDrawingRoute) {
@@ -20,7 +20,7 @@ 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';
import 'map_widget.dart/left_main_menu_icons.dart';
import 'map_widget.dart/main_bottom_menu_map.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';
@@ -65,9 +65,9 @@ class MapPagePassenger extends StatelessWidget {
// OsmMapPassengerWidget(),
leftMainMenuIcons(),
// PaymobPackage(),
const PickerIconOnMap(),
const RpCenterPin(),
// PickerAnimtionContainerFormPlaces(),
const MainBottomMenuMap(),
const RoutePlannerSheet(),
// NewMainBottomSheet(),
buttomSheetMapPage(),
@@ -200,27 +200,4 @@ class CancelRidePageShow extends StatelessWidget {
}
}
class PickerIconOnMap extends StatelessWidget {
const PickerIconOnMap({
super.key,
});
@override
Widget build(BuildContext context) {
return GetBuilder<MapEngineController>(
builder: (controller) => controller.isPickerShown
? Positioned(
bottom: Get.height * .2,
top: 0,
left: 0,
right: 0,
child: controller.isPickerShown
? const Icon(
Icons.add_location,
color: Colors.purple,
)
: const SizedBox(),
)
: const SizedBox());
}
}
// PickerIconOnMap was replaced by RpCenterPin (see route_planner/rp_center_pin.dart).
@@ -7,7 +7,6 @@ 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 'package:siro_rider/views/widgets/elevated_btn.dart';
import '../../../constant/colors.dart';
import '../../../constant/style.dart';
@@ -15,7 +14,6 @@ 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 '../../../controller/home/map/ride_state.dart';
import '../../../main.dart';
// ---------------------------------------------------
@@ -119,55 +117,94 @@ class _SearchFieldState extends State<_SearchField> {
@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: 8.0),
child: Row(
children: [
Expanded(
child: TextFormField(
controller: widget.controller.placeDestinationController,
onChanged: _onSearchChanged,
decoration: InputDecoration(
hintText: widget.controller.hintTextDestinationPoint,
hintStyle: AppStyle.subtitle.copyWith(color: Colors.grey[600]),
prefixIcon: Icon(Icons.search, color: AppColor.primaryColor),
suffixIcon: widget
.controller.placeDestinationController.text.isNotEmpty
? IconButton(
icon: Icon(Icons.clear, color: Colors.grey[400]),
onPressed: () {
widget.controller.placeDestinationController.clear();
},
)
: null,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 10.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide.none,
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,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(color: AppColor.primaryColor),
),
filled: true,
fillColor: Colors.grey[50],
),
),
),
const SizedBox(width: 8.0),
IconButton(
onPressed: () {
widget.mapEngine.changeMainBottomMenuMap();
widget.mapEngine.changePickerShown();
},
icon: Icon(Icons.location_on_outlined,
color: AppColor.accentColor, size: 30),
tooltip: widget.rideLifecycle.isAnotherOreder
? 'Pick destination on map'.tr
: 'Pick on map'.tr,
),
],
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),
),
),
),
],
),
),
);
}
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.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';
@@ -16,64 +15,104 @@ GetBuilder<LocationSearchController> formSearchPlacesStart() {
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: 8.0),
child: Row(
children: [
Expanded(
child: TextFormField(
controller: controller.placeStartController,
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: Colors.grey[600]),
prefixIcon:
Icon(Icons.search, color: AppColor.primaryColor),
suffixIcon: controller.placeStartController.text.isNotEmpty
? IconButton(
icon: Icon(Icons.clear, color: Colors.grey[400]),
onPressed: () {
controller.placeStartController.clear();
controller.clearPlacesStart();
},
)
: null,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 10.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide.none,
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,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(color: AppColor.primaryColor),
),
filled: true,
fillColor: Colors.grey[50],
),
),
),
const SizedBox(width: 8.0),
IconButton(
onPressed: () {
controller.passengerStartLocationFromMap = true;
mapEngine.changeMainBottomMenuMap();
mapEngine.changePickerShown();
},
icon: Icon(Icons.location_on_outlined,
color: AppColor.accentColor, size: 30),
tooltip: 'Pick start point on map'.tr,
),
],
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(
@@ -96,7 +135,8 @@ GetBuilder<LocationSearchController> formSearchPlacesStart() {
var address = res['address'] ?? 'Details not available';
return ListTile(
leading: const Icon(Icons.place, size: 30, color: Colors.grey),
leading:
const Icon(Icons.place, size: 30, color: Colors.grey),
title: Text(title,
style: AppStyle.subtitle
.copyWith(fontWeight: FontWeight.w500)),
@@ -106,8 +146,8 @@ GetBuilder<LocationSearchController> formSearchPlacesStart() {
var latitude = res['latitude'];
var longitude = res['longitude'];
if (latitude != null && longitude != null) {
controller.passengerLocation =
LatLng(double.parse(latitude), double.parse(longitude));
controller.passengerLocation = LatLng(
double.parse(latitude), double.parse(longitude));
controller.placeStartController.text = title;
controller.clearPlacesStart();
mapEngine.changeMainBottomMenuMap();
@@ -1,7 +1,6 @@
import 'dart:ui' show ImageFilter;
import 'package:siro_rider/print.dart';
import 'package:siro_rider/views/widgets/my_textField.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_rider/constant/box_name.dart';
@@ -74,7 +73,6 @@ class _D {
],
stops: const [0.0, 0.5, 1.0],
);
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -1050,65 +1048,91 @@ class _OrderTypeButton extends StatelessWidget {
Widget build(BuildContext context) {
final rideLifecycle = Get.find<RideLifecycleController>();
final bool isOther = mapEngine.isAnotherOreder;
final Color accent =
isOther ? Colors.indigo.shade500 : AppColor.primaryColor;
const MaterialColor accent = Colors.indigo;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
showCupertinoModalPopup(
context: context,
builder: (ctx) => CupertinoActionSheet(
title: Text('Select Order Type'.tr),
actions: [
CupertinoActionSheetAction(
child: Text('I want to order for myself'.tr),
onPressed: () {
mapEngine.changeisAnotherOreder(false);
rideLifecycle.isAnotherOreder = false;
Navigator.pop(ctx);
},
),
CupertinoActionSheetAction(
child: Text('I want to order for someone else'.tr),
onPressed: () {
mapEngine.changeisAnotherOreder(true);
rideLifecycle.isAnotherOreder = true;
Navigator.pop(ctx);
},
),
],
cancelButton: CupertinoActionSheetAction(
isDefaultAction: true,
onPressed: () => Navigator.pop(ctx),
child: Text('Cancel'.tr)),
),
);
},
borderRadius: BorderRadius.circular(_D.radiusInner),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(_D.radiusInner)),
child: Row(
children: [
Icon(isOther ? Icons.person_rounded : Icons.group_rounded,
color: accent, size: 17),
const SizedBox(width: 14),
Expanded(
child: Text(
isOther
? 'Order for myself'.tr
: 'Order for someone else'.tr,
void select(bool other) {
if (mapEngine.isAnotherOreder == other) return;
mapEngine.changeisAnotherOreder(other);
rideLifecycle.isAnotherOreder = other;
}
Widget segment(
{required bool selected,
required IconData icon,
required String label,
required VoidCallback onTap}) {
return Expanded(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(_D.radiusInner - 2),
child: AnimatedContainer(
duration: _D.fast,
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: selected ? accent.shade500 : Colors.transparent,
borderRadius: BorderRadius.circular(_D.radiusInner - 2),
boxShadow: selected
? [
BoxShadow(
color: accent.withValues(alpha: 0.35),
blurRadius: 10,
offset: const Offset(0, 3)),
]
: null,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon,
color: selected ? Colors.white : Colors.grey.shade500,
size: 16),
const SizedBox(width: 8),
Flexible(
child: Text(
label,
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: accent,
fontSize: 13.5,
fontWeight: FontWeight.w600))),
],
color: selected ? Colors.white : Colors.grey.shade600,
fontSize: 13,
fontWeight:
selected ? FontWeight.w700 : FontWeight.w500,
),
),
),
],
),
),
),
),
);
}
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(_D.radiusInner),
),
child: Row(
children: [
segment(
selected: !isOther,
icon: Icons.person_rounded,
label: 'Order for myself'.tr,
onTap: () => select(false),
),
const SizedBox(width: 4),
segment(
selected: isOther,
icon: Icons.group_rounded,
label: 'Order for someone else'.tr,
onTap: () => select(true),
),
],
),
);
}
@@ -946,9 +946,9 @@ class NavigationController extends GetxController
Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams);
try {
final response = await http
.get(saasUri, headers: {'x-api-key': Env.mapSaasKey})
.timeout(const Duration(seconds: 15));
final response = await http.get(saasUri, headers: {
'x-api-key': Env.mapSaasKey
}).timeout(const Duration(seconds: 15));
if (response.statusCode != 200) {
if (retryCount < 2) {
@@ -1042,8 +1042,7 @@ class NavigationController extends GetxController
if (routeSteps.isNotEmpty) {
currentInstruction = routeSteps[0]['text'] ?? "";
currentManeuverModifier =
ManeuverSign.fromValue(routeSteps[0]['sign']);
currentManeuverModifier = ManeuverSign.fromValue(routeSteps[0]['sign']);
nextInstruction = routeSteps.length > 1
? (langCode == 'ar'
? "ثم ${routeSteps[1]['text']}"
@@ -0,0 +1,10 @@
/// Route Planner — a self-contained, controller-agnostic front-end for the
/// ride-planning surface (bottom sheet, map-pick overlay, and center pin).
///
/// Public entry points:
/// • [RoutePlannerSheet] — replaces the old `MainBottomMenuMap`.
/// • [RpCenterPin] — replaces the old `PickerIconOnMap`.
library;
export 'rp_center_pin.dart';
export 'rp_sheet.dart';
@@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../../../constant/colors.dart';
import '../../../constant/table_names.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';
import '../../../print.dart';
import '../../widgets/mydialoug.dart';
/// All controller side-effects for the route planner live here, so the
/// widgets stay declarative. This is a faithful port of the behavior that
/// previously lived inside `form_search_*.dart` and `main_bottom_Menu_map`.
///
/// No controller is modified — this only *calls* their existing API.
class RpActions {
RpActions._();
static LocationSearchController get _s =>
Get.find<LocationSearchController>();
static MapEngineController get _map => Get.find<MapEngineController>();
static RideLifecycleController get _ride =>
Get.find<RideLifecycleController>();
// ── Destination ─────────────────────────────────────────────────────────
static void pickDestinationOnMap() {
_map.changeMainBottomMenuMap();
_map.changePickerShown();
}
static Future<void> selectDestination(int index, dynamic res) async {
final lat = res['latitude'];
final lng = res['longitude'];
if (lat == null || lng == null) {
Toast.show(Get.context!, 'Invalid location data', AppColor.redColor);
return;
}
final String title =
(res['name_ar'] ?? res['name'] ?? 'Unknown Place').toString();
await sql.insertMapLocation({
'latitude': lat,
'longitude': lng,
'name': title,
'rate': 'N/A',
'createdAt': DateTime.now().toIso8601String(),
}, TableName.recentLocations);
final dest =
LatLng(double.parse(lat.toString()), double.parse(lng.toString()));
if (_ride.isAnotherOreder) {
_s.myDestination = dest;
_s.clearPlacesDestination();
await _ride.getDirectionMap(
'${_s.passengerLocation.latitude},${_s.passengerLocation.longitude}',
'${_s.myDestination.latitude},${_s.myDestination.longitude}');
_map.isPickerShown = false;
_s.passengerStartLocationFromMap = false;
_map.changeMainBottomMenuMap();
_ride.showBottomSheet1();
} else {
_s.passengerLocation = _s.newMyLocation;
_s.myDestination = dest;
_s.convertHintTextDestinationNewPlaces(index);
_s.clearPlacesDestination();
_map.changeMainBottomMenuMap();
_s.passengerStartLocationFromMap = true;
_map.isPickerShown = true;
_ride.getDirectionMap(
'${_s.passengerLocation.latitude},${_s.passengerLocation.longitude}',
'${_s.myDestination.latitude},${_s.myDestination.longitude}');
}
}
// ── Origin / pickup ───────────────────────────────────────────────────────
/// Map-pick for the *other person's* pickup (order-for-someone-else mode).
static void pickOtherPickupOnMap() {
_s.passengerStartLocationFromMap = true;
_map.changeMainBottomMenuMap();
_map.changePickerShown();
}
/// Map-pick for my own start point (updates the pickup without re-routing).
static void pickMyStartOnMap() {
_s.startLocationFromMap = true;
_map.changeMainBottomMenuMap();
_map.changePickerShown();
}
/// Selecting a searched start point (other person's pickup).
static void selectStart(dynamic res) {
final lat = res['latitude'];
final lng = res['longitude'];
if (lat == null || lng == null) return;
final String title =
(res['name_ar'] ?? res['name'] ?? 'Unknown Place').toString();
_s.passengerLocation =
LatLng(double.parse(lat.toString()), double.parse(lng.toString()));
_s.placeStartController.text = title;
_s.clearPlacesStart();
_s.update();
}
// ── Stops (menu waypoints) ────────────────────────────────────────────────
static void addStop() => _s.addMenuWaypoint();
static void removeStop(int index) => _s.removeMenuWaypoint(index);
static void pickStopOnMap(int index) => _s.startPickingWaypointOnMap(index);
// ── Home / Work quick actions ─────────────────────────────────────────────
static void pickHomeOrWork({required bool isWork}) {
if (isWork) {
_s.workLocationFromMap = true;
} else {
_s.homeLocationFromMap = true;
}
_map.changeMainBottomMenuMap();
_map.changePickerShown();
}
static void changeHomeOrWork({required bool isWork}) {
MyDialog().getDialog(
isWork ? 'Change Work location ?'.tr : 'Change Home location ?'.tr,
'',
() => pickHomeOrWork(isWork: isWork),
);
}
/// Navigate to a saved Home/Work location and open the ride sheet.
static Future<void> goToSaved(String boxName, String hint) async {
try {
final locationString = box.read(boxName).toString();
final parts = locationString.split(',');
final latLng =
LatLng(double.parse(parts[0].trim()), double.parse(parts[1].trim()));
_s.hintTextDestinationPoint = hint.tr;
_map.changeMainBottomMenuMap();
await _ride.getDirectionMap(
'${_s.passengerLocation.latitude},${_s.passengerLocation.longitude}',
'${latLng.latitude},${latLng.longitude}',
);
_s.currentLocationToFormPlaces = false;
_s.clearPlacesDestination();
_s.passengerStartLocationFromMap = false;
_map.isPickerShown = false;
_ride.showBottomSheet1();
} catch (e) {
Log.print('RpActions.goToSaved error: $e');
Toast.show(Get.context!, 'Failed to get location'.tr, AppColor.redColor);
}
}
// ── Favorites / recents ───────────────────────────────────────────────────
static Future<void> addFavorite(
BuildContext context, dynamic lat, dynamic lng, String title) async {
if (lat == null || lng == null) {
Toast.show(context, 'Invalid location data', AppColor.redColor);
return;
}
await sql.insertMapLocation({
'latitude': lat,
'longitude': lng,
'name': title,
'rate': 'N/A',
}, TableName.placesFavorite);
if (!context.mounted) return;
Toast.show(context, '$title ${'Saved Successfully'.tr}',
AppColor.primaryColor);
}
/// Route to a stored place (recent or favorite) after confirmation.
static Future<void> goToStoredPlace(dynamic place) async {
await _s.getLocation();
await _ride.getDirectionMap(
'${_s.passengerLocation.latitude},${_s.passengerLocation.longitude}',
'${place['latitude']},${place['longitude']}',
);
_ride.showBottomSheet1();
}
}
@@ -0,0 +1,192 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/home/map/location_search_controller.dart';
import '../../../controller/home/map/map_engine_controller.dart';
import 'rp_theme.dart';
/// A crisp, animated map-center marker shown while picking a point.
/// Replaces the old flat purple `Icons.add_location`.
///
/// The pin's tip is anchored to the exact camera center; it bobs gently and
/// casts a pulsing ground shadow so the user can tell precisely where the
/// point will land.
class RpCenterPin extends StatefulWidget {
const RpCenterPin({super.key});
@override
State<RpCenterPin> createState() => _RpCenterPinState();
}
class _RpCenterPinState extends State<RpCenterPin>
with SingleTickerProviderStateMixin {
late final AnimationController _c;
static const double _w = 44;
static const double _h = 58; // circle + tail
@override
void initState() {
super.initState();
_c = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1400),
)..repeat(reverse: true);
}
@override
void dispose() {
_c.dispose();
super.dispose();
}
({Color color, IconData icon}) _style(LocationSearchController s) {
if (s.isPickingWaypoint) {
return (color: RP.stop1, icon: Icons.add_location_alt_rounded);
}
if (s.passengerStartLocationFromMap) {
return (color: RP.origin, icon: Icons.person_pin_circle_rounded);
}
if (s.workLocationFromMap) {
return (color: const Color(0xFF2563EB), icon: Icons.work_rounded);
}
if (s.homeLocationFromMap) {
return (color: const Color(0xFFF59E0B), icon: Icons.home_rounded);
}
if (s.startLocationFromMap) {
return (color: RP.origin, icon: Icons.trip_origin_rounded);
}
return (color: RP.destination, icon: Icons.location_on_rounded);
}
@override
Widget build(BuildContext context) {
return GetBuilder<MapEngineController>(
builder: (mapEngine) {
if (!mapEngine.isPickerShown) return const SizedBox.shrink();
return GetBuilder<LocationSearchController>(
builder: (s) {
final style = _style(s);
// The map fills the area above the bottom sheet band.
return Positioned(
top: 0,
left: 0,
right: 0,
bottom: Get.height * .2,
child: IgnorePointer(
child: Center(
child: AnimatedBuilder(
animation: _c,
builder: (context, _) {
final double t = Curves.easeInOut.transform(_c.value);
final double lift = 4 * t; // gentle bob
return SizedBox(
width: _w + 20,
height: _h + 24,
child: Stack(
alignment: Alignment.center,
children: [
// Ground shadow at the exact camera center.
Align(
alignment: Alignment.center,
child: Transform.translate(
offset: const Offset(0, 0),
child: Container(
width: 20 - 6 * t,
height: 7 - 2 * t,
decoration: BoxDecoration(
color: Colors.black
.withValues(alpha: 0.28 - 0.10 * t),
borderRadius: BorderRadius.circular(999),
),
),
),
),
// The pin — tip anchored to center, bobbing up.
Align(
alignment: Alignment.center,
child: Transform.translate(
offset: Offset(0, -(_h / 2) - lift),
child: CustomPaint(
size: const Size(_w, _h),
painter: _PinPainter(style.color),
child: SizedBox(
width: _w,
height: _h,
child: Align(
alignment: const Alignment(0, -0.34),
child: Icon(style.icon,
color: Colors.white, size: 20),
),
),
),
),
),
],
),
);
},
),
),
),
);
},
);
},
);
}
}
/// Draws a teardrop pin: a filled circle with a smooth pointed tail and a
/// white outline, plus a soft drop shadow.
class _PinPainter extends CustomPainter {
final Color color;
_PinPainter(this.color);
@override
void paint(Canvas canvas, Size size) {
final double w = size.width;
final double r = w / 2;
final Offset c = Offset(w / 2, r);
final double tipY = size.height;
final Path path = Path();
// Left side sweeping from the tip up around the circle and back.
path.moveTo(w / 2, tipY);
path.quadraticBezierTo(0, r + r * 0.55, 0, r);
path.arcTo(
Rect.fromCircle(center: c, radius: r),
3.1415926, // start at left (pi)
3.1415926, // sweep pi (top half) → to right
false,
);
path.quadraticBezierTo(w, r + r * 0.55, w / 2, tipY);
path.close();
// Shadow
canvas.drawShadow(path, Colors.black.withValues(alpha: 0.5), 4.0, false);
// Fill
final Paint fill = Paint()
..color = color
..style = PaintingStyle.fill
..isAntiAlias = true;
canvas.drawPath(path, fill);
// White outline
final Paint stroke = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 3
..isAntiAlias = true;
canvas.drawPath(path, stroke);
// Inner white disc behind the icon for contrast.
final Paint disc = Paint()..color = Colors.white.withValues(alpha: 0.22);
canvas.drawCircle(c, r * 0.62, disc);
}
@override
bool shouldRepaint(covariant _PinPainter old) => old.color != color;
}
@@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/table_names.dart';
import '../../../controller/home/map/location_search_controller.dart';
import '../../../main.dart';
import '../../widgets/elevated_btn.dart';
import '../../widgets/mydialoug.dart';
import 'rp_actions.dart';
import 'rp_theme.dart';
/// Star button that opens the saved "Favorite Places" list.
class RpFavoritesButton extends StatelessWidget {
const RpFavoritesButton({super.key});
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(RP.rChip),
onTap: () async {
final List favorites = await sql.getAllData(TableName.placesFavorite);
Get.defaultDialog(
title: 'Favorite Places'.tr,
titleStyle: RP.title,
backgroundColor: RP.sheetElevated,
radius: RP.rCard,
content: SizedBox(
width: Get.width * .85,
height: 300,
child: favorites.isEmpty
? Center(
child: Text('No favorite places yet!'.tr, style: RP.body))
: ListView.separated(
itemCount: favorites.length,
separatorBuilder: (_, __) =>
Divider(height: 1, color: RP.divider),
itemBuilder: (context, index) {
final place = favorites[index];
return ListTile(
leading: const Icon(Icons.star_rounded,
color: Color(0xFFF59E0B), size: 22),
title: Text(place['name'].toString(),
style: RP.fieldValue),
trailing: IconButton(
icon: Icon(Icons.delete_outline_rounded,
color: RP.destination),
onPressed: () async {
await sql.deleteData(
TableName.placesFavorite, place['id']);
Get.back();
},
),
onTap: () async {
Get.back();
await RpActions.goToStoredPlace(place);
},
);
},
),
),
confirm:
MyElevatedButton(title: 'Back'.tr, onPressed: () => Get.back()),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star_rounded, color: Color(0xFFF59E0B), size: 20),
const SizedBox(width: 8),
Text('Favorite Places'.tr,
style: TextStyle(
fontSize: 13.5,
fontWeight: FontWeight.w700,
color: RP.textBody)),
],
),
),
);
}
}
/// Horizontal strip of recently-used destinations.
class RpRecentsRow extends StatelessWidget {
final LocationSearchController locationSearch;
const RpRecentsRow({super.key, required this.locationSearch});
@override
Widget build(BuildContext context) {
if (locationSearch.recentPlaces.isEmpty) return const SizedBox.shrink();
return SizedBox(
height: 40,
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 20),
itemCount: locationSearch.recentPlaces.length,
separatorBuilder: (_, __) => const SizedBox(width: 8),
itemBuilder: (context, index) => _RpRecentChip(
place: locationSearch.recentPlaces[index],
),
),
);
}
}
class _RpRecentChip extends StatelessWidget {
final dynamic place;
const _RpRecentChip({required this.place});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(RP.rChip),
onTap: () {
MyDialog().getDialog(
'Are you want to go this site'.tr,
' ',
() async => RpActions.goToStoredPlace(place),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: RP.chipFill,
borderRadius: BorderRadius.circular(RP.rChip),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.history_rounded, size: 15, color: RP.textMuted),
const SizedBox(width: 7),
Text(
(place['name'] ?? '').toString(),
style: TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w600,
color: RP.textBody),
),
],
),
),
),
);
}
}
@@ -0,0 +1,221 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'rp_search_results.dart';
import 'rp_theme.dart';
/// A single row of the route timeline (origin, a stop, or the destination).
///
/// It renders a colored node, an inline search field, a "pick on map" button
/// and — when the parent feeds it live results — a dropdown beneath it.
///
/// Fully controller-agnostic: all data + side effects arrive via callbacks.
class RpLocationField extends StatefulWidget {
final Color accent;
/// Leading node icon. Ignored when [badge] is provided.
final IconData icon;
/// When set, the node shows this text (e.g. a stop number) instead of [icon].
final String? badge;
final TextEditingController controller;
final String hintText;
/// Live results to show under the field (empty ⇒ hidden).
final List results;
/// Fired (debounced) with the trimmed query once it is long enough.
final void Function(String query) onQuery;
/// Fired (debounced) when the field is cleared / emptied.
final VoidCallback onEmpty;
final VoidCallback onPickOnMap;
final void Function(int index) onResultTap;
final void Function(int index)? onResultFavorite;
/// Optional trailing "remove" affordance (used by stops).
final VoidCallback? onRemove;
/// Whether the field participates in search. When false it behaves as a
/// read-only value chip that still opens the map picker on tap.
final bool searchable;
const RpLocationField({
super.key,
required this.accent,
required this.icon,
required this.controller,
required this.hintText,
required this.results,
required this.onQuery,
required this.onEmpty,
required this.onPickOnMap,
required this.onResultTap,
this.badge,
this.onResultFavorite,
this.onRemove,
this.searchable = true,
});
@override
State<RpLocationField> createState() => _RpLocationFieldState();
}
class _RpLocationFieldState extends State<RpLocationField> {
Timer? _debounce;
void _rebuild() => setState(() {});
@override
void initState() {
super.initState();
widget.controller.addListener(_rebuild);
}
@override
void dispose() {
_debounce?.cancel();
widget.controller.removeListener(_rebuild);
super.dispose();
}
void _onChanged(String raw) {
_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 400), () {
final q = raw.trim();
if (q.length > 2) {
widget.onQuery(q);
} else if (q.isEmpty) {
widget.onEmpty();
}
});
}
@override
Widget build(BuildContext context) {
final bool hasText = widget.controller.text.trim().isNotEmpty;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: RP.fieldFill,
borderRadius: BorderRadius.circular(RP.rField),
border: Border.all(
color: hasText
? widget.accent.withValues(alpha: 0.45)
: Colors.transparent,
width: 1.4,
),
),
child: Row(
children: [
_node(),
const SizedBox(width: 10),
Expanded(
child: TextField(
controller: widget.controller,
onChanged: widget.searchable ? _onChanged : null,
readOnly: !widget.searchable,
onTap: widget.searchable ? null : widget.onPickOnMap,
style: RP.fieldValue,
cursorColor: widget.accent,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
isDense: true,
border: InputBorder.none,
hintText: widget.hintText,
hintStyle: RP.fieldHint,
contentPadding: const EdgeInsets.symmetric(vertical: 8),
),
),
),
if (hasText && widget.searchable)
_iconBtn(
icon: Icons.close_rounded,
tint: RP.textMuted,
onTap: () {
widget.controller.clear();
widget.onEmpty();
},
),
if (widget.onRemove != null)
_iconBtn(
icon: Icons.delete_outline_rounded,
tint: RP.destination,
onTap: widget.onRemove!,
),
_iconBtn(
icon: Icons.map_rounded,
tint: widget.accent,
filled: true,
onTap: widget.onPickOnMap,
tooltip: 'Pick on map'.tr,
),
],
),
),
RpResultsList(
results: widget.results,
accent: widget.accent,
onTap: widget.onResultTap,
onFavorite: widget.onResultFavorite,
),
],
);
}
Widget _node() {
return Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: widget.accent,
shape: BoxShape.circle,
boxShadow: RP.glow(widget.accent),
),
child: Center(
child: widget.badge != null
? Text(
widget.badge!,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w800,
),
)
: Icon(widget.icon, color: Colors.white, size: 17),
),
);
}
Widget _iconBtn({
required IconData icon,
required Color tint,
required VoidCallback onTap,
bool filled = false,
String? tooltip,
}) {
final btn = InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(RP.rChip),
child: Container(
margin: const EdgeInsets.only(left: 4),
width: 34,
height: 34,
decoration: BoxDecoration(
color: filled ? tint.withValues(alpha: 0.12) : Colors.transparent,
borderRadius: BorderRadius.circular(RP.rChip),
),
child: Icon(icon, color: tint, size: 19),
),
);
return tooltip == null ? btn : Tooltip(message: tooltip, child: btn);
}
}
@@ -0,0 +1,415 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intaleq_maps/intaleq_maps.dart';
import '../../../constant/box_name.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 '../../../print.dart';
import '../../widgets/error_snakbar.dart';
import 'rp_theme.dart';
/// The confirmation surface shown while the user is dragging the map to
/// choose a point (destination / pickup / stop / home / work).
///
/// The state machine in [_confirm] is a faithful port of the original
/// `_MapPickerOverlay._onConfirmTap` — only the presentation changed.
class RpMapPickOverlay extends StatelessWidget {
const RpMapPickOverlay({super.key});
// ── Mode resolution ─────────────────────────────────────────────────────
_PickMode _mode(LocationSearchController s) {
final rideLifecycle = Get.find<RideLifecycleController>();
if (s.isPickingWaypoint) return _PickMode.stop;
if (s.passengerStartLocationFromMap) {
return rideLifecycle.isAnotherOreder
? _PickMode.pickupOther
: _PickMode.pickup;
}
if (s.startLocationFromMap) return _PickMode.start;
if (s.workLocationFromMap) return _PickMode.work;
if (s.homeLocationFromMap) return _PickMode.home;
return _PickMode.destination;
}
String _title(_PickMode m, LocationSearchController s) {
switch (m) {
case _PickMode.stop:
return 'Move map to set stop'.tr +
' ${s.pickingWaypointIndex + 1}'.tr;
case _PickMode.pickupOther:
return 'Now set the pickup point for the other person'.tr;
case _PickMode.pickup:
return 'Move map to your pickup point'.tr;
case _PickMode.start:
return 'Move map to set start location'.tr;
case _PickMode.work:
return 'Move map to your work location'.tr;
case _PickMode.home:
return 'Move map to your home location'.tr;
case _PickMode.destination:
return 'Move map to select destination'.tr;
}
}
String _confirmLabel(_PickMode m) {
switch (m) {
case _PickMode.stop:
return 'Set as Stop'.tr;
case _PickMode.pickup:
case _PickMode.pickupOther:
return 'Confirm Pickup Location'.tr;
case _PickMode.work:
return 'Set as Work'.tr;
case _PickMode.home:
return 'Set as Home'.tr;
case _PickMode.start:
case _PickMode.destination:
return 'Set Destination'.tr;
}
}
IconData _icon(_PickMode m) {
switch (m) {
case _PickMode.stop:
return Icons.add_location_alt_rounded;
case _PickMode.pickup:
case _PickMode.pickupOther:
return Icons.person_pin_circle_rounded;
case _PickMode.work:
return Icons.work_rounded;
case _PickMode.home:
return Icons.home_rounded;
case _PickMode.start:
case _PickMode.destination:
return Icons.location_on_rounded;
}
}
Color _color(_PickMode m) {
switch (m) {
case _PickMode.stop:
return RP.stop1;
case _PickMode.pickup:
case _PickMode.pickupOther:
return RP.origin;
case _PickMode.work:
return const Color(0xFF2563EB);
case _PickMode.home:
return const Color(0xFFF59E0B);
case _PickMode.start:
return RP.origin;
case _PickMode.destination:
return RP.destination;
}
}
@override
Widget build(BuildContext context) {
final mapEngine = Get.find<MapEngineController>();
return GetBuilder<LocationSearchController>(
builder: (s) {
final mode = _mode(s);
final color = _color(mode);
final String name = s.currentLocationString.trim();
final String coords =
'${s.newMyLocation.latitude.toStringAsFixed(5)}, ${s.newMyLocation.longitude.toStringAsFixed(5)}';
return Positioned(
left: 16,
right: 16,
bottom: Get.height * .035,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Instruction banner
Container(
width: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(RP.rCard),
boxShadow: RP.glow(color, intensity: 0.4),
),
child: Row(
children: [
Icon(_icon(mode), color: Colors.white, size: 20),
const SizedBox(width: 12),
Expanded(
child: Text(
_title(mode, s),
style: const TextStyle(
color: Colors.white,
fontSize: 14.5,
fontWeight: FontWeight.w700,
),
),
),
],
),
),
const SizedBox(height: 10),
// Selection card
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
decoration: BoxDecoration(
color: RP.sheet,
borderRadius: BorderRadius.circular(RP.rSheet),
boxShadow: RP.cardShadow,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
shape: BoxShape.circle,
),
child: Icon(Icons.gps_fixed_rounded,
color: color, size: 19),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name.isEmpty ? 'Selected point'.tr : name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: RP.fieldValue,
),
const SizedBox(height: 2),
Text(coords,
style: TextStyle(
fontSize: 12, color: RP.textMuted)),
],
),
),
],
),
const SizedBox(height: 18),
Row(
children: [
Expanded(
flex: 2,
child: _CancelButton(
onTap: () => _cancel(mapEngine, s),
),
),
const SizedBox(width: 12),
Expanded(
flex: 3,
child: _ConfirmButton(
label: _confirmLabel(mode),
color: color,
onTap: () => _confirm(context, mapEngine, s),
),
),
],
),
],
),
),
],
),
);
},
);
}
// ── Cancel (faithful port) ───────────────────────────────────────────────
void _cancel(MapEngineController mapEngine, LocationSearchController s) {
mapEngine.isPickerShown = false;
s.passengerStartLocationFromMap = false;
s.startLocationFromMap = false;
s.workLocationFromMap = false;
s.homeLocationFromMap = false;
s.isPickingWaypoint = false;
s.pickingWaypointIndex = -1;
if (!mapEngine.isMainBottomMenuMap) {
mapEngine.isMainBottomMenuMap = true;
mapEngine.mainBottomMenuMapHeight = Get.height * .22;
}
mapEngine.update();
s.update();
}
// ── Confirm (faithful port of _onConfirmTap) ─────────────────────────────
Future<void> _confirm(BuildContext context, MapEngineController mapEngine,
LocationSearchController s) async {
final rideLifecycle = Get.find<RideLifecycleController>();
Log.print(
'🔘 RP confirm: isPickingWaypoint=${s.isPickingWaypoint}, newMyLocation=${s.newMyLocation}');
await Future.delayed(const Duration(milliseconds: 280));
final LatLng cam =
LatLng(s.newMyLocation.latitude, s.newMyLocation.longitude);
if (s.isPickingWaypoint && s.pickingWaypointIndex >= 0) {
s.setMenuWaypointFromMap(s.pickingWaypointIndex, cam);
mySnackbarSuccess('Waypoint has been set successfully'.tr);
return;
}
mapEngine.clearPolyline();
rideLifecycle.data = [];
if (s.passengerStartLocationFromMap) {
final LatLng start = cam;
s.newStartPointLocation = start;
s.passengerStartLocationFromMap = false;
mapEngine.isPickerShown = false;
s.currentLocationToFormPlaces = false;
s.placesDestination = [];
s.clearPlacesStart();
s.clearPlacesDestination();
mapEngine.isMainBottomMenuMap = true;
mapEngine.mainBottomMenuMapHeight = Get.height * .22;
mapEngine.update();
s.update();
await rideLifecycle.getDirectionMap(
'${start.latitude},${start.longitude}',
'${s.myDestination.latitude},${s.myDestination.longitude}');
rideLifecycle.showBottomSheet1();
return;
}
if (s.startLocationFromMap) {
final LatLng start = cam;
s.newMyLocation = start;
s.newStartPointLocation = start;
s.hintTextStartPoint =
'${start.latitude.toStringAsFixed(4)} , ${start.longitude.toStringAsFixed(4)}';
s.startLocationFromMap = false;
mapEngine.isPickerShown = false;
s.update();
mapEngine.update();
return;
}
if (s.workLocationFromMap) {
box.write(BoxName.addWork,
'${cam.latitude.toStringAsFixed(4)} , ${cam.longitude.toStringAsFixed(4)}');
s.hintTextDestinationPoint = 'To Work'.tr;
s.workLocationFromMap = false;
mapEngine.isPickerShown = false;
s.update();
mapEngine.update();
mySnackbarSuccess('Work Saved'.tr);
return;
}
if (s.homeLocationFromMap) {
box.write(BoxName.addHome,
'${cam.latitude.toStringAsFixed(4)} , ${cam.longitude.toStringAsFixed(4)}');
s.hintTextDestinationPoint = 'To Home'.tr;
s.homeLocationFromMap = false;
mapEngine.isPickerShown = false;
s.update();
mapEngine.update();
mySnackbarSuccess('Home Saved'.tr);
return;
}
// Default: destination chosen on map ⇒ chain into pickup selection.
s.myDestination = cam;
s.hintTextDestinationPoint =
'${cam.latitude.toStringAsFixed(4)} , ${cam.longitude.toStringAsFixed(4)}';
s.placesDestination = [];
s.placeDestinationController.clear();
s.passengerStartLocationFromMap = true;
mapEngine.isPickerShown = true;
s.update();
mapEngine.update();
try {
if (rideLifecycle.isAnotherOreder) {
await mapEngine.mapController?.animateCamera(CameraUpdate.newLatLng(
LatLng(s.newStartPointLocation.latitude,
s.newStartPointLocation.longitude)));
} else {
await mapEngine.mapController?.animateCamera(CameraUpdate.newLatLng(
LatLng(s.passengerLocation.latitude,
s.passengerLocation.longitude)));
}
} catch (e) {
Log.print('RP confirm animate error: $e');
}
}
}
enum _PickMode { destination, pickup, pickupOther, start, stop, home, work }
class _CancelButton extends StatelessWidget {
final VoidCallback onTap;
const _CancelButton({required this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(RP.rField),
child: Container(
height: 52,
alignment: Alignment.center,
decoration: BoxDecoration(
color: RP.fieldFill,
borderRadius: BorderRadius.circular(RP.rField),
),
child: Text(
'Cancel'.tr,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: RP.textBody,
),
),
),
);
}
}
class _ConfirmButton extends StatelessWidget {
final String label;
final Color color;
final VoidCallback onTap;
const _ConfirmButton(
{required this.label, required this.color, required this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(RP.rField),
child: Container(
height: 52,
alignment: Alignment.center,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(RP.rField),
boxShadow: RP.glow(color, intensity: 0.4),
),
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w800,
color: Colors.white,
),
),
),
);
}
}
@@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
import 'rp_theme.dart';
/// A dropdown-style list of geocoding search results.
///
/// Purely presentational: the parent supplies the raw result maps
/// (the shape returned by [LocationSearchController.getPlaces] /
/// [getPlacesStart]) and the tap callbacks.
class RpResultsList extends StatelessWidget {
final List results;
final Color accent;
final void Function(int index) onTap;
final void Function(int index)? onFavorite;
final double maxHeight;
const RpResultsList({
super.key,
required this.results,
required this.accent,
required this.onTap,
this.onFavorite,
this.maxHeight = 260,
});
@override
Widget build(BuildContext context) {
final bool show = results.isNotEmpty;
return AnimatedSize(
duration: RP.fast,
curve: RP.ease,
child: !show
? const SizedBox(width: double.infinity)
: Container(
constraints: BoxConstraints(maxHeight: maxHeight),
margin: const EdgeInsets.only(top: 8),
decoration: BoxDecoration(
color: RP.sheetElevated,
borderRadius: BorderRadius.circular(RP.rCard),
border: Border.all(color: RP.divider),
boxShadow: RP.cardShadow,
),
clipBehavior: Clip.antiAlias,
child: ListView.separated(
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 4),
physics: const ClampingScrollPhysics(),
itemCount: results.length,
separatorBuilder: (_, __) => Divider(
height: 1,
thickness: 1,
indent: 56,
color: RP.divider,
),
itemBuilder: (context, index) {
final res = results[index];
final String title =
(res['name_ar'] ?? res['name'] ?? 'Unknown Place')
.toString();
final String address =
(res['address'] ?? '').toString();
return InkWell(
onTap: () => onTap(index),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 11),
child: Row(
children: [
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.12),
shape: BoxShape.circle,
),
child: Icon(Icons.place_rounded,
size: 18, color: accent),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: RP.textStrong,
),
),
if (address.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
address,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12, color: RP.textMuted),
),
],
],
),
),
if (onFavorite != null)
IconButton(
visualDensity: VisualDensity.compact,
icon: Icon(Icons.favorite_border_rounded,
size: 20, color: RP.textMuted),
onPressed: () => onFavorite!(index),
),
],
),
),
);
},
),
),
);
}
}
@@ -0,0 +1,722 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.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/elevated_btn.dart';
import '../../widgets/my_textField.dart';
import 'rp_actions.dart';
import 'rp_favorites.dart';
import 'rp_location_field.dart';
import 'rp_map_pick_overlay.dart';
import 'rp_theme.dart';
/// The route-planning surface anchored to the bottom of the map.
///
/// • While the user drags the map to place a point → [RpMapPickOverlay].
/// • Otherwise → a collapsed "Where to?" card or the full planner.
///
/// Drop-in replacement for the old `MainBottomMenuMap`. Uses only the
/// existing controller APIs; nothing in the controllers changed.
class RoutePlannerSheet extends StatelessWidget {
const RoutePlannerSheet({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<MapEngineController>(
builder: (mapEngine) {
if (mapEngine.isPickerShown) return const RpMapPickOverlay();
return Positioned(
left: 14,
right: 14,
bottom: Get.height * .03,
child: AnimatedContainer(
duration: RP.medium,
curve: RP.ease,
constraints: BoxConstraints(
maxHeight: mapEngine.isMainBottomMenuMap
? Get.height * 0.42
: Get.height * 0.72,
),
decoration: BoxDecoration(
color: RP.sheet,
borderRadius: BorderRadius.circular(RP.rSheet),
boxShadow: RP.sheetShadow,
border: Border.all(color: RP.divider),
),
clipBehavior: Clip.antiAlias,
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: mapEngine.isMainBottomMenuMap
? const _Collapsed()
: const _Expanded(),
),
),
);
},
);
}
}
// ══════════════════════════════════════════════════════════════════════════
// COLLAPSED
// ══════════════════════════════════════════════════════════════════════════
class _Collapsed extends StatelessWidget {
const _Collapsed();
@override
Widget build(BuildContext context) {
final String firstName =
(box.read(BoxName.name)?.toString().split(' ').first ?? '').trim();
final mapEngine = Get.find<MapEngineController>();
final rideLifecycle = Get.find<RideLifecycleController>();
return GetBuilder<LocationSearchController>(
builder: (locationSearch) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 12),
RP.grabber(),
const SizedBox(height: 14),
InkWell(
onTap: mapEngine.changeMainBottomMenuMap,
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: RP.brand,
borderRadius: BorderRadius.circular(16),
boxShadow: RP.glow(RP.brand),
),
child: const Icon(Icons.search_rounded,
color: Colors.white, size: 24),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(children: [
TextSpan(
text: '${'Where to'.tr} ',
style: TextStyle(
fontSize: 16.5,
fontWeight: FontWeight.w700,
color: RP.textStrong),
),
if (firstName.isNotEmpty)
TextSpan(
text: firstName,
style: TextStyle(
fontSize: 16.5,
fontWeight: FontWeight.w800,
color: RP.brand),
),
const TextSpan(text: '؟'),
]),
),
if (!rideLifecycle.noCarString) ...[
const SizedBox(height: 2),
Text('Tap to search your destination'.tr,
style:
TextStyle(fontSize: 12, color: RP.textMuted)),
],
],
),
),
Icon(Icons.keyboard_arrow_up_rounded,
color: RP.textMuted, size: 26),
],
),
),
),
const SizedBox(height: 14),
RpRecentsRow(locationSearch: locationSearch),
const SizedBox(height: 16),
],
);
},
);
}
}
// ══════════════════════════════════════════════════════════════════════════
// EXPANDED
// ══════════════════════════════════════════════════════════════════════════
class _Expanded extends StatelessWidget {
const _Expanded();
@override
Widget build(BuildContext context) {
final mapEngine = Get.find<MapEngineController>();
return GetBuilder<LocationSearchController>(
builder: (s) {
final rideLifecycle = Get.find<RideLifecycleController>();
final bool isOther = rideLifecycle.isAnotherOreder;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 12),
Center(child: RP.grabber()),
// Header
Padding(
padding: const EdgeInsets.fromLTRB(20, 14, 14, 6),
child: Row(
children: [
Text('Plan Your Route'.tr, style: RP.title),
const Spacer(),
_CircleIconButton(
icon: Icons.keyboard_arrow_down_rounded,
onTap: mapEngine.changeMainBottomMenuMap,
),
],
),
),
// Order-type toggle
Padding(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 4),
child: _OrderTypeToggle(isOther: isOther),
),
// Route timeline
Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_originField(s, isOther),
const SizedBox(height: 10),
...List.generate(
s.activeMenuWaypointCount,
(i) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _StopRow(index: i, s: s),
),
),
if (s.activeMenuWaypointCount < 2) ...[
_AddStopButton(onTap: RpActions.addStop),
const SizedBox(height: 10),
],
_destinationField(s),
],
),
),
const SizedBox(height: 6),
_Divider(),
// Quick access
_SectionHeader(
title: 'Quick Access'.tr,
trailing: const RpFavoritesButton(),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(child: _homeWorkButton(isWork: false)),
const SizedBox(width: 12),
Expanded(child: _homeWorkButton(isWork: true)),
],
),
),
const SizedBox(height: 14),
RpRecentsRow(locationSearch: s),
const SizedBox(height: 6),
_Divider(),
// Advanced
_SectionHeader(title: 'Advanced Tools'.tr),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _WhatsAppLinkButton(locationSearch: s),
),
const SizedBox(height: 22),
],
);
},
);
}
// ── Origin ──────────────────────────────────────────────────────────────
Widget _originField(LocationSearchController s, bool isOther) {
if (isOther) {
return RpLocationField(
accent: RP.origin,
icon: Icons.person_pin_circle_rounded,
controller: s.placeStartController,
hintText: 'Search for a starting point'.tr,
results: s.placesStart,
onQuery: (_) => s.getPlacesStart(),
onEmpty: s.clearPlacesStart,
onPickOnMap: RpActions.pickOtherPickupOnMap,
onResultTap: (i) => RpActions.selectStart(s.placesStart[i]),
);
}
// Regular order: origin = current location, changeable on the map.
return RpLocationField(
accent: RP.origin,
icon: Icons.my_location_rounded,
controller: s.placeStartController,
hintText: s.currentLocationString,
results: const [],
searchable: false,
onQuery: (_) {},
onEmpty: () {},
onPickOnMap: RpActions.pickMyStartOnMap,
onResultTap: (_) {},
);
}
// ── Destination ─────────────────────────────────────────────────────────
Widget _destinationField(LocationSearchController s) {
final mapEngine = Get.find<MapEngineController>();
return RpLocationField(
accent: RP.destination,
icon: Icons.flag_rounded,
controller: s.placeDestinationController,
hintText: s.hintTextDestinationPoint,
results: s.placesDestination,
onQuery: (_) {
s.getPlaces();
mapEngine.changeHeightPlaces();
},
onEmpty: () {
s.clearPlacesDestination();
mapEngine.changeHeightPlaces();
},
onPickOnMap: RpActions.pickDestinationOnMap,
onResultTap: (i) => RpActions.selectDestination(i, s.placesDestination[i]),
onResultFavorite: (i) {
final res = s.placesDestination[i];
RpActions.addFavorite(
Get.context!,
res['latitude'],
res['longitude'],
(res['name_ar'] ?? res['name'] ?? 'Unknown Place').toString(),
);
},
);
}
Widget _homeWorkButton({required bool isWork}) {
final String saved = isWork
? (box.read(BoxName.addWork)?.toString() ?? 'addWork')
: (box.read(BoxName.addHome)?.toString() ?? 'addHome');
final bool isSet = isWork ? saved != 'addWork' : saved != 'addHome';
final Color accent =
isWork ? const Color(0xFF2563EB) : const Color(0xFFF59E0B);
return _QuickTile(
icon: isWork ? Icons.work_rounded : Icons.home_rounded,
accent: accent,
label: isWork
? (isSet ? 'To Work'.tr : 'Add Work'.tr)
: (isSet ? 'To Home'.tr : 'Add Home'.tr),
onTap: () {
if (isSet) {
RpActions.goToSaved(
isWork ? BoxName.addWork : BoxName.addHome,
isWork ? 'To Work' : 'To Home');
} else {
RpActions.pickHomeOrWork(isWork: isWork);
}
},
onLongPress: () => RpActions.changeHomeOrWork(isWork: isWork),
);
}
}
// ── Stop row ────────────────────────────────────────────────────────────────
class _StopRow extends StatelessWidget {
final int index;
final LocationSearchController s;
const _StopRow({required this.index, required this.s});
@override
Widget build(BuildContext context) {
final Color accent = RP.stopColor(index);
final bool isSet = s.menuWaypoints[index] != null;
final String name =
isSet ? s.menuWaypointNames[index] : '${'Stop'.tr} ${index + 1}';
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: RP.fieldFill,
borderRadius: BorderRadius.circular(RP.rField),
border: Border.all(
color: isSet ? accent.withValues(alpha: 0.45) : Colors.transparent,
width: 1.4,
),
),
child: Row(
children: [
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: accent,
shape: BoxShape.circle,
boxShadow: RP.glow(accent),
),
child: Center(
child: Text('${index + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w800)),
),
),
const SizedBox(width: 10),
Expanded(
child: InkWell(
onTap: () => RpActions.pickStopOnMap(index),
child: Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: isSet
? RP.fieldValue
: RP.fieldHint,
),
),
),
InkWell(
onTap: () => RpActions.removeStop(index),
borderRadius: BorderRadius.circular(RP.rChip),
child: Container(
width: 34,
height: 34,
alignment: Alignment.center,
child: Icon(Icons.delete_outline_rounded,
color: RP.destination, size: 19),
),
),
InkWell(
onTap: () => RpActions.pickStopOnMap(index),
borderRadius: BorderRadius.circular(RP.rChip),
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(RP.rChip),
),
child: Icon(Icons.map_rounded, color: accent, size: 19),
),
),
],
),
);
}
}
class _AddStopButton extends StatelessWidget {
final VoidCallback onTap;
const _AddStopButton({required this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(RP.rField),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 13),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(RP.rField),
border: Border.all(
color: RP.stop1.withValues(alpha: 0.5),
width: 1.4,
style: BorderStyle.solid),
color: RP.stop1.withValues(alpha: 0.06),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add_location_alt_outlined,
color: RP.stop1, size: 18),
const SizedBox(width: 8),
Text('Add a Stop'.tr,
style: TextStyle(
color: RP.stop1,
fontSize: 13.5,
fontWeight: FontWeight.w700)),
],
),
),
);
}
}
// ── Order-type toggle ────────────────────────────────────────────────────────
class _OrderTypeToggle extends StatelessWidget {
final bool isOther;
const _OrderTypeToggle({required this.isOther});
@override
Widget build(BuildContext context) {
final mapEngine = Get.find<MapEngineController>();
final rideLifecycle = Get.find<RideLifecycleController>();
void select(bool other) {
if (mapEngine.isAnotherOreder == other) return;
mapEngine.changeisAnotherOreder(other);
rideLifecycle.isAnotherOreder = other;
// The expanded planner rebuilds on LocationSearchController updates, so
// refresh it here to swap the origin field (self ⇄ other) instantly.
Get.find<LocationSearchController>().update();
}
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: RP.chipFill,
borderRadius: BorderRadius.circular(RP.rField),
),
child: Row(
children: [
_seg(
selected: !isOther,
icon: Icons.person_rounded,
label: 'Order for myself'.tr,
onTap: () => select(false),
),
const SizedBox(width: 4),
_seg(
selected: isOther,
icon: Icons.group_rounded,
label: 'Order for someone else'.tr,
onTap: () => select(true),
),
],
),
);
}
Widget _seg({
required bool selected,
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return Expanded(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(RP.rChip),
child: AnimatedContainer(
duration: RP.fast,
padding: const EdgeInsets.symmetric(vertical: 11),
decoration: BoxDecoration(
color: selected ? const Color(0xFF4F46E5) : Colors.transparent,
borderRadius: BorderRadius.circular(RP.rChip),
boxShadow: selected
? RP.glow(const Color(0xFF4F46E5), intensity: 0.35)
: null,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon,
size: 16,
color: selected ? Colors.white : RP.textMuted),
const SizedBox(width: 7),
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12.5,
fontWeight:
selected ? FontWeight.w700 : FontWeight.w600,
color: selected ? Colors.white : RP.textBody,
),
),
),
],
),
),
),
),
);
}
}
// ── Quick tile (home/work) ───────────────────────────────────────────────────
class _QuickTile extends StatelessWidget {
final IconData icon;
final Color accent;
final String label;
final VoidCallback onTap;
final VoidCallback? onLongPress;
const _QuickTile({
required this.icon,
required this.accent,
required this.label,
required this.onTap,
this.onLongPress,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
onLongPress: onLongPress,
borderRadius: BorderRadius.circular(RP.rField),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(RP.rField),
),
child: Row(
children: [
Icon(icon, color: accent, size: 20),
const SizedBox(width: 10),
Flexible(
child: Text(label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: accent,
fontSize: 13.5,
fontWeight: FontWeight.w700)),
),
],
),
),
);
}
}
// ── WhatsApp link ─────────────────────────────────────────────────────────────
class _WhatsAppLinkButton extends StatelessWidget {
final LocationSearchController locationSearch;
const _WhatsAppLinkButton({required this.locationSearch});
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(RP.rField),
onTap: () {
Get.dialog(
AlertDialog(
backgroundColor: RP.sheetElevated,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(RP.rCard)),
title: Text('WhatsApp Location Extractor'.tr, style: RP.title),
content: Form(
key: locationSearch.sosFormKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
MyTextForm(
controller: locationSearch.whatsAppLocationText,
label: 'Location Link'.tr,
type: TextInputType.url,
hint: 'https://maps.app.goo.gl/...'),
const SizedBox(height: 16),
MyElevatedButton(
title: 'Go to this location'.tr,
onPressed: () => locationSearch.goToWhatappLocation()),
],
),
),
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
decoration: BoxDecoration(
color: const Color(0xFF25D366).withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(RP.rField),
),
child: Row(
children: [
const Icon(Icons.link_rounded,
color: Color(0xFF1FA855), size: 20),
const SizedBox(width: 12),
Expanded(
child: Text('Paste WhatsApp location link'.tr,
style: const TextStyle(
color: Color(0xFF1FA855),
fontSize: 13.5,
fontWeight: FontWeight.w700)),
),
],
),
),
);
}
}
// ── Small shared bits ─────────────────────────────────────────────────────────
class _SectionHeader extends StatelessWidget {
final String title;
final Widget? trailing;
const _SectionHeader({required this.title, this.trailing});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 4, 12, 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title.toUpperCase(), style: RP.sectionLabel),
if (trailing != null) trailing!,
],
),
);
}
}
class _Divider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 1,
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
color: RP.divider,
);
}
}
class _CircleIconButton extends StatelessWidget {
final IconData icon;
final VoidCallback onTap;
const _CircleIconButton({required this.icon, required this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(RP.rPill),
child: Container(
width: 38,
height: 38,
decoration: BoxDecoration(color: RP.fieldFill, shape: BoxShape.circle),
child: Icon(icon, size: 24, color: RP.textBody),
),
);
}
}
@@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/colors.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// Route Planner — Design System
///
/// A single source of truth for the visual language of the ride-planning
/// surface. Inspired by best-in-class ride-hailing apps: calm neutrals,
/// a strong navy primary, and semantic colors for the route timeline
/// (origin = green, stops = amber/violet, destination = red).
///
/// This file is pure presentation. It never touches controllers.
/// ─────────────────────────────────────────────────────────────────────────
class RP {
RP._();
// ── Radii ──────────────────────────────────────────────────────────────
static const double rSheet = 28;
static const double rCard = 20;
static const double rField = 16;
static const double rChip = 14;
static const double rPill = 999;
// ── Motion ─────────────────────────────────────────────────────────────
static const Duration fast = Duration(milliseconds: 180);
static const Duration medium = Duration(milliseconds: 360);
static const Curve ease = Curves.easeOutCubic;
// ── Semantic route colors ──────────────────────────────────────────────
static const Color origin = Color(0xFF16A34A); // green — pickup / start
static const Color destination = Color(0xFFEF4444); // red — drop-off
static const Color stop1 = Color(0xFFF59E0B); // amber — first stop
static const Color stop2 = Color(0xFF7C3AED); // violet — second stop
static Color get brand => AppColor.primaryColor;
static Color stopColor(int index) => index == 0 ? stop1 : stop2;
// ── Surfaces (theme-aware) ─────────────────────────────────────────────
static bool get isDark => Get.isDarkMode;
static Color get sheet =>
isDark ? const Color(0xFF1C1D21) : const Color(0xFFFFFFFF);
static Color get sheetElevated =>
isDark ? const Color(0xFF26272C) : const Color(0xFFFFFFFF);
static Color get fieldFill =>
isDark ? const Color(0xFF2A2B31) : const Color(0xFFF4F5F7);
static Color get chipFill =>
isDark ? const Color(0xFF2A2B31) : const Color(0xFFF1F3F6);
static Color get divider =>
isDark ? const Color(0xFF33343A) : const Color(0xFFECEEF1);
// ── Text ───────────────────────────────────────────────────────────────
static Color get textStrong =>
isDark ? const Color(0xFFF5F6F8) : const Color(0xFF10131A);
static Color get textBody =>
isDark ? const Color(0xFFC7CAD1) : const Color(0xFF3A3F4A);
static Color get textMuted =>
isDark ? const Color(0xFF8A8E98) : const Color(0xFF8B9099);
// ── Elevation ──────────────────────────────────────────────────────────
static List<BoxShadow> get sheetShadow => [
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.45 : 0.12),
blurRadius: 40,
spreadRadius: -6,
offset: const Offset(0, -8),
),
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.30 : 0.06),
blurRadius: 14,
spreadRadius: -4,
offset: const Offset(0, -2),
),
];
static List<BoxShadow> get cardShadow => [
BoxShadow(
color: Colors.black.withValues(alpha: isDark ? 0.35 : 0.08),
blurRadius: 24,
spreadRadius: -8,
offset: const Offset(0, 8),
),
];
static List<BoxShadow> glow(Color c, {double intensity = 0.35}) => [
BoxShadow(
color: c.withValues(alpha: intensity),
blurRadius: 16,
spreadRadius: -2,
offset: const Offset(0, 4),
),
];
// ── Text styles ────────────────────────────────────────────────────────
static TextStyle get title => TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
letterSpacing: -0.4,
color: textStrong,
);
static TextStyle get sectionLabel => TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w700,
letterSpacing: 0.4,
color: textMuted,
);
static TextStyle get fieldValue => TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: textStrong,
);
static TextStyle get fieldHint => TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: textMuted,
);
static TextStyle get body => TextStyle(
fontSize: 13.5,
fontWeight: FontWeight.w500,
color: textBody,
);
// ── Reusable pieces ────────────────────────────────────────────────────
/// A short grabber handle used at the top of sheets.
static Widget grabber() => Container(
width: 42,
height: 5,
decoration: BoxDecoration(
color: isDark ? Colors.white24 : Colors.black12,
borderRadius: BorderRadius.circular(RP.rPill),
),
);
}
+1
View File
@@ -0,0 +1 @@
{"distance":621.885,"duration":160,"trafficAwareDuration":160,"trafficFactor":1,"startName":"منزل أبو هارون، ضاحية البستان، قضاء الزرقاء، الزرقاء","endName":"مسجد الحاج عايد الغويري، قضاء الزرقاء، الزرقاء","points":"ey~bEalc{Eu@[{BwAaAq@e@WiBs@oB{@OIEEGZMfA?ND|A?LWxAYrAc@dBUdB","bbox":[36.065952,32.111711,36.068957,32.114943],"alternatives":[]}