Update: 2026-07-10 16:09:37
This commit is contained in:
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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":[]}
|
||||
Reference in New Issue
Block a user