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();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onStyleLoaded() async {
|
Future<void> onStyleLoaded() async {
|
||||||
Log.print('🗺️ Siro Map Style Loaded. Initializing...');
|
Log.print('🗺️ Siro Map Style Loaded. Initializing...');
|
||||||
isStyleLoaded = true;
|
isStyleLoaded = true;
|
||||||
await _loadMapIcons();
|
await _loadMapIcons();
|
||||||
@@ -207,7 +207,12 @@ class MapEngineController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int? _getImageSize(String id) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3719,18 +3719,18 @@ class RideLifecycleController extends GetxController {
|
|||||||
|
|
||||||
// بناء الـ markers أولاً ثم وضعها معاً مع polylines في تحديث واحد
|
// بناء الـ markers أولاً ثم وضعها معاً مع polylines في تحديث واحد
|
||||||
final Set<Marker> newMarkers = {
|
final Set<Marker> newMarkers = {
|
||||||
|
// لا نمرر InfoWindow هنا: صور A/B تحتوي الحرف أصلاً، وأي textField
|
||||||
|
// إضافي يرسم حرفاً مكرراً فوق الدبوس بعد إصلاح الخطوط في الـ SDK.
|
||||||
Marker(
|
Marker(
|
||||||
markerId: const MarkerId('start'),
|
markerId: const MarkerId('start'),
|
||||||
position: startLoc,
|
position: startLoc,
|
||||||
icon: InlqBitmap.fromAsset('assets/images/A.png'),
|
icon: InlqBitmap.fromStyleImage('start_icon'),
|
||||||
infoWindow: const InfoWindow(title: 'A'),
|
|
||||||
anchor: const Offset(0.5, 1.0),
|
anchor: const Offset(0.5, 1.0),
|
||||||
),
|
),
|
||||||
Marker(
|
Marker(
|
||||||
markerId: const MarkerId('end'),
|
markerId: const MarkerId('end'),
|
||||||
position: endLoc,
|
position: endLoc,
|
||||||
icon: InlqBitmap.fromAsset('assets/images/b.png'),
|
icon: InlqBitmap.fromStyleImage('end_icon'),
|
||||||
infoWindow: const InfoWindow(title: 'B'),
|
|
||||||
anchor: const Offset(0.5, 1.0),
|
anchor: const Offset(0.5, 1.0),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@@ -3742,11 +3742,9 @@ class RideLifecycleController extends GetxController {
|
|||||||
newMarkers.add(Marker(
|
newMarkers.add(Marker(
|
||||||
markerId: MarkerId('waypoint_$i'),
|
markerId: MarkerId('waypoint_$i'),
|
||||||
position: wp,
|
position: wp,
|
||||||
icon: InlqBitmap.fromAsset(isFirstWaypoint
|
icon: InlqBitmap.fromStyleImage(isFirstWaypoint
|
||||||
? 'assets/images/A.png'
|
? 'start_icon'
|
||||||
: 'assets/images/b.png'),
|
: 'end_icon'),
|
||||||
infoWindow:
|
|
||||||
InfoWindow(title: isFirstWaypoint ? 'Stop 1' : 'Stop 2'),
|
|
||||||
anchor: const Offset(0.5, 1.0),
|
anchor: const Offset(0.5, 1.0),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -3758,27 +3756,12 @@ class RideLifecycleController extends GetxController {
|
|||||||
Log.print(
|
Log.print(
|
||||||
'✅ FIX P1 v2: ${newMarkers.length} markers placed — start: $startLoc, end: $endLoc');
|
'✅ 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 bottomSheet();
|
||||||
|
|
||||||
await mapEngine.playRouteAnimation(
|
await mapEngine.playRouteAnimation(
|
||||||
mapEngine.polylineCoordinates, mapEngine.lastComputedBounds);
|
mapEngine.polylineCoordinates, mapEngine.lastComputedBounds);
|
||||||
|
|
||||||
|
mapEngine.update(); // Make sure the map UI rebuilds with the new markers
|
||||||
update();
|
update();
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
if (isDrawingRoute) {
|
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/cash_confirm_bottom_page.dart';
|
||||||
import 'map_widget.dart/google_map_passenger_widget.dart';
|
import 'map_widget.dart/google_map_passenger_widget.dart';
|
||||||
import 'map_widget.dart/left_main_menu_icons.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 'map_widget.dart/map_menu_widget.dart';
|
||||||
import '../../controller/functions/package_info.dart';
|
import '../../controller/functions/package_info.dart';
|
||||||
import 'map_widget.dart/passengerRideLoctionWidget.dart';
|
import 'map_widget.dart/passengerRideLoctionWidget.dart';
|
||||||
@@ -65,9 +65,9 @@ class MapPagePassenger extends StatelessWidget {
|
|||||||
// OsmMapPassengerWidget(),
|
// OsmMapPassengerWidget(),
|
||||||
leftMainMenuIcons(),
|
leftMainMenuIcons(),
|
||||||
// PaymobPackage(),
|
// PaymobPackage(),
|
||||||
const PickerIconOnMap(),
|
const RpCenterPin(),
|
||||||
// PickerAnimtionContainerFormPlaces(),
|
// PickerAnimtionContainerFormPlaces(),
|
||||||
const MainBottomMenuMap(),
|
const RoutePlannerSheet(),
|
||||||
// NewMainBottomSheet(),
|
// NewMainBottomSheet(),
|
||||||
|
|
||||||
buttomSheetMapPage(),
|
buttomSheetMapPage(),
|
||||||
@@ -200,27 +200,4 @@ class CancelRidePageShow extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PickerIconOnMap extends StatelessWidget {
|
// PickerIconOnMap was replaced by RpCenterPin (see route_planner/rp_center_pin.dart).
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:get/get.dart';
|
|||||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||||
import 'package:siro_rider/constant/box_name.dart';
|
import 'package:siro_rider/constant/box_name.dart';
|
||||||
import 'package:siro_rider/constant/table_names.dart';
|
import 'package:siro_rider/constant/table_names.dart';
|
||||||
import 'package:siro_rider/views/widgets/elevated_btn.dart';
|
|
||||||
|
|
||||||
import '../../../constant/colors.dart';
|
import '../../../constant/colors.dart';
|
||||||
import '../../../constant/style.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/location_search_controller.dart';
|
||||||
import '../../../controller/home/map/map_engine_controller.dart';
|
import '../../../controller/home/map/map_engine_controller.dart';
|
||||||
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
import '../../../controller/home/map/ride_lifecycle_controller.dart';
|
||||||
import '../../../controller/home/map/ride_state.dart';
|
|
||||||
import '../../../main.dart';
|
import '../../../main.dart';
|
||||||
|
|
||||||
// ---------------------------------------------------
|
// ---------------------------------------------------
|
||||||
@@ -119,55 +117,94 @@ class _SearchFieldState extends State<_SearchField> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
const Color accent = Color(0xFFEF4444); // matches the red destination dot
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0),
|
||||||
child: Row(
|
child: Container(
|
||||||
children: [
|
padding: const EdgeInsets.all(6),
|
||||||
Expanded(
|
decoration: BoxDecoration(
|
||||||
child: TextFormField(
|
gradient: LinearGradient(colors: [
|
||||||
controller: widget.controller.placeDestinationController,
|
accent.withOpacity(0.08),
|
||||||
onChanged: _onSearchChanged,
|
accent.withOpacity(0.03),
|
||||||
decoration: InputDecoration(
|
]),
|
||||||
hintText: widget.controller.hintTextDestinationPoint,
|
borderRadius: BorderRadius.circular(16),
|
||||||
hintStyle: AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
border: Border.all(color: accent.withOpacity(0.35), width: 1.4),
|
||||||
prefixIcon: Icon(Icons.search, color: AppColor.primaryColor),
|
),
|
||||||
suffixIcon: widget
|
child: Row(
|
||||||
.controller.placeDestinationController.text.isNotEmpty
|
children: [
|
||||||
? IconButton(
|
Container(
|
||||||
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
width: 34,
|
||||||
onPressed: () {
|
height: 34,
|
||||||
widget.controller.placeDestinationController.clear();
|
decoration: BoxDecoration(
|
||||||
},
|
color: accent,
|
||||||
)
|
shape: BoxShape.circle,
|
||||||
: null,
|
boxShadow: [
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
BoxShadow(
|
||||||
horizontal: 16.0, vertical: 10.0),
|
color: accent.withOpacity(0.4),
|
||||||
border: OutlineInputBorder(
|
blurRadius: 10,
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
offset: const Offset(0, 3)),
|
||||||
borderSide: BorderSide.none,
|
],
|
||||||
|
),
|
||||||
|
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: 6.0),
|
||||||
const SizedBox(width: 8.0),
|
InkWell(
|
||||||
IconButton(
|
borderRadius: BorderRadius.circular(12),
|
||||||
onPressed: () {
|
onTap: () {
|
||||||
widget.mapEngine.changeMainBottomMenuMap();
|
widget.mapEngine.changeMainBottomMenuMap();
|
||||||
widget.mapEngine.changePickerShown();
|
widget.mapEngine.changePickerShown();
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.location_on_outlined,
|
child: Tooltip(
|
||||||
color: AppColor.accentColor, size: 30),
|
message: widget.rideLifecycle.isAnotherOreder
|
||||||
tooltip: widget.rideLifecycle.isAnotherOreder
|
? 'Pick destination on map'.tr
|
||||||
? 'Pick destination on map'.tr
|
: 'Pick 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:get/get.dart';
|
||||||
import 'package:intaleq_maps/intaleq_maps.dart';
|
import 'package:intaleq_maps/intaleq_maps.dart';
|
||||||
|
|
||||||
import '../../../constant/colors.dart';
|
|
||||||
import '../../../constant/style.dart';
|
import '../../../constant/style.dart';
|
||||||
import '../../../controller/home/map/location_search_controller.dart';
|
import '../../../controller/home/map/location_search_controller.dart';
|
||||||
import '../../../controller/home/map/map_engine_controller.dart';
|
import '../../../controller/home/map/map_engine_controller.dart';
|
||||||
@@ -16,64 +15,104 @@ GetBuilder<LocationSearchController> formSearchPlacesStart() {
|
|||||||
id: 'start_point_form',
|
id: 'start_point_form',
|
||||||
builder: (controller) {
|
builder: (controller) {
|
||||||
final mapEngine = Get.find<MapEngineController>();
|
final mapEngine = Get.find<MapEngineController>();
|
||||||
|
const Color accent = Color(0xFF16A34A); // green: "their" pickup point
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
padding:
|
||||||
child: Row(
|
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0),
|
||||||
children: [
|
child: Container(
|
||||||
Expanded(
|
padding: const EdgeInsets.all(6),
|
||||||
child: TextFormField(
|
decoration: BoxDecoration(
|
||||||
controller: controller.placeStartController,
|
gradient: LinearGradient(colors: [
|
||||||
onChanged: (value) {
|
accent.withOpacity(0.08),
|
||||||
if (controller.placeStartController.text.length > 2) {
|
accent.withOpacity(0.03),
|
||||||
controller.getPlacesStart();
|
]),
|
||||||
} else if (controller.placeStartController.text.isEmpty) {
|
borderRadius: BorderRadius.circular(16),
|
||||||
controller.clearPlacesStart();
|
border: Border.all(color: accent.withOpacity(0.35), width: 1.4),
|
||||||
}
|
),
|
||||||
},
|
child: Row(
|
||||||
decoration: InputDecoration(
|
children: [
|
||||||
hintText: 'Search for a starting point'.tr,
|
Container(
|
||||||
hintStyle:
|
width: 34,
|
||||||
AppStyle.subtitle.copyWith(color: Colors.grey[600]),
|
height: 34,
|
||||||
prefixIcon:
|
decoration: BoxDecoration(
|
||||||
Icon(Icons.search, color: AppColor.primaryColor),
|
color: accent,
|
||||||
suffixIcon: controller.placeStartController.text.isNotEmpty
|
shape: BoxShape.circle,
|
||||||
? IconButton(
|
boxShadow: [
|
||||||
icon: Icon(Icons.clear, color: Colors.grey[400]),
|
BoxShadow(
|
||||||
onPressed: () {
|
color: accent.withOpacity(0.4),
|
||||||
controller.placeStartController.clear();
|
blurRadius: 10,
|
||||||
controller.clearPlacesStart();
|
offset: const Offset(0, 3)),
|
||||||
},
|
],
|
||||||
)
|
),
|
||||||
: null,
|
child: const Icon(Icons.person_pin_circle_rounded,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
color: Colors.white, size: 18),
|
||||||
horizontal: 16.0, vertical: 10.0),
|
),
|
||||||
border: OutlineInputBorder(
|
const SizedBox(width: 10),
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
Expanded(
|
||||||
borderSide: BorderSide.none,
|
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: 6.0),
|
||||||
const SizedBox(width: 8.0),
|
InkWell(
|
||||||
IconButton(
|
borderRadius: BorderRadius.circular(12),
|
||||||
onPressed: () {
|
onTap: () {
|
||||||
controller.passengerStartLocationFromMap = true;
|
controller.passengerStartLocationFromMap = true;
|
||||||
mapEngine.changeMainBottomMenuMap();
|
mapEngine.changeMainBottomMenuMap();
|
||||||
mapEngine.changePickerShown();
|
mapEngine.changePickerShown();
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.location_on_outlined,
|
child: Tooltip(
|
||||||
color: AppColor.accentColor, size: 30),
|
message: 'Pick start point on map'.tr,
|
||||||
tooltip: '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(
|
AnimatedContainer(
|
||||||
@@ -96,7 +135,8 @@ GetBuilder<LocationSearchController> formSearchPlacesStart() {
|
|||||||
var address = res['address'] ?? 'Details not available';
|
var address = res['address'] ?? 'Details not available';
|
||||||
|
|
||||||
return ListTile(
|
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,
|
title: Text(title,
|
||||||
style: AppStyle.subtitle
|
style: AppStyle.subtitle
|
||||||
.copyWith(fontWeight: FontWeight.w500)),
|
.copyWith(fontWeight: FontWeight.w500)),
|
||||||
@@ -106,8 +146,8 @@ GetBuilder<LocationSearchController> formSearchPlacesStart() {
|
|||||||
var latitude = res['latitude'];
|
var latitude = res['latitude'];
|
||||||
var longitude = res['longitude'];
|
var longitude = res['longitude'];
|
||||||
if (latitude != null && longitude != null) {
|
if (latitude != null && longitude != null) {
|
||||||
controller.passengerLocation =
|
controller.passengerLocation = LatLng(
|
||||||
LatLng(double.parse(latitude), double.parse(longitude));
|
double.parse(latitude), double.parse(longitude));
|
||||||
controller.placeStartController.text = title;
|
controller.placeStartController.text = title;
|
||||||
controller.clearPlacesStart();
|
controller.clearPlacesStart();
|
||||||
mapEngine.changeMainBottomMenuMap();
|
mapEngine.changeMainBottomMenuMap();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'dart:ui' show ImageFilter;
|
import 'dart:ui' show ImageFilter;
|
||||||
import 'package:siro_rider/print.dart';
|
import 'package:siro_rider/print.dart';
|
||||||
import 'package:siro_rider/views/widgets/my_textField.dart';
|
import 'package:siro_rider/views/widgets/my_textField.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:siro_rider/constant/box_name.dart';
|
import 'package:siro_rider/constant/box_name.dart';
|
||||||
@@ -74,7 +73,6 @@ class _D {
|
|||||||
],
|
],
|
||||||
stops: const [0.0, 0.5, 1.0],
|
stops: const [0.0, 0.5, 1.0],
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -1050,65 +1048,91 @@ class _OrderTypeButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final rideLifecycle = Get.find<RideLifecycleController>();
|
final rideLifecycle = Get.find<RideLifecycleController>();
|
||||||
final bool isOther = mapEngine.isAnotherOreder;
|
final bool isOther = mapEngine.isAnotherOreder;
|
||||||
final Color accent =
|
const MaterialColor accent = Colors.indigo;
|
||||||
isOther ? Colors.indigo.shade500 : AppColor.primaryColor;
|
|
||||||
|
|
||||||
return Material(
|
void select(bool other) {
|
||||||
color: Colors.transparent,
|
if (mapEngine.isAnotherOreder == other) return;
|
||||||
child: InkWell(
|
mapEngine.changeisAnotherOreder(other);
|
||||||
onTap: () {
|
rideLifecycle.isAnotherOreder = other;
|
||||||
showCupertinoModalPopup(
|
}
|
||||||
context: context,
|
|
||||||
builder: (ctx) => CupertinoActionSheet(
|
Widget segment(
|
||||||
title: Text('Select Order Type'.tr),
|
{required bool selected,
|
||||||
actions: [
|
required IconData icon,
|
||||||
CupertinoActionSheetAction(
|
required String label,
|
||||||
child: Text('I want to order for myself'.tr),
|
required VoidCallback onTap}) {
|
||||||
onPressed: () {
|
return Expanded(
|
||||||
mapEngine.changeisAnotherOreder(false);
|
child: Material(
|
||||||
rideLifecycle.isAnotherOreder = false;
|
color: Colors.transparent,
|
||||||
Navigator.pop(ctx);
|
child: InkWell(
|
||||||
},
|
onTap: onTap,
|
||||||
),
|
borderRadius: BorderRadius.circular(_D.radiusInner - 2),
|
||||||
CupertinoActionSheetAction(
|
child: AnimatedContainer(
|
||||||
child: Text('I want to order for someone else'.tr),
|
duration: _D.fast,
|
||||||
onPressed: () {
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
mapEngine.changeisAnotherOreder(true);
|
decoration: BoxDecoration(
|
||||||
rideLifecycle.isAnotherOreder = true;
|
color: selected ? accent.shade500 : Colors.transparent,
|
||||||
Navigator.pop(ctx);
|
borderRadius: BorderRadius.circular(_D.radiusInner - 2),
|
||||||
},
|
boxShadow: selected
|
||||||
),
|
? [
|
||||||
],
|
BoxShadow(
|
||||||
cancelButton: CupertinoActionSheetAction(
|
color: accent.withValues(alpha: 0.35),
|
||||||
isDefaultAction: true,
|
blurRadius: 10,
|
||||||
onPressed: () => Navigator.pop(ctx),
|
offset: const Offset(0, 3)),
|
||||||
child: Text('Cancel'.tr)),
|
]
|
||||||
),
|
: null,
|
||||||
);
|
),
|
||||||
},
|
child: Row(
|
||||||
borderRadius: BorderRadius.circular(_D.radiusInner),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
child: Container(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
Icon(icon,
|
||||||
decoration: BoxDecoration(
|
color: selected ? Colors.white : Colors.grey.shade500,
|
||||||
color: accent.withValues(alpha: 0.08),
|
size: 16),
|
||||||
borderRadius: BorderRadius.circular(_D.radiusInner)),
|
const SizedBox(width: 8),
|
||||||
child: Row(
|
Flexible(
|
||||||
children: [
|
child: Text(
|
||||||
Icon(isOther ? Icons.person_rounded : Icons.group_rounded,
|
label,
|
||||||
color: accent, size: 17),
|
textAlign: TextAlign.center,
|
||||||
const SizedBox(width: 14),
|
maxLines: 1,
|
||||||
Expanded(
|
overflow: TextOverflow.ellipsis,
|
||||||
child: Text(
|
|
||||||
isOther
|
|
||||||
? 'Order for myself'.tr
|
|
||||||
: 'Order for someone else'.tr,
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: accent,
|
color: selected ? Colors.white : Colors.grey.shade600,
|
||||||
fontSize: 13.5,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w600))),
|
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);
|
Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await http
|
final response = await http.get(saasUri, headers: {
|
||||||
.get(saasUri, headers: {'x-api-key': Env.mapSaasKey})
|
'x-api-key': Env.mapSaasKey
|
||||||
.timeout(const Duration(seconds: 15));
|
}).timeout(const Duration(seconds: 15));
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
if (retryCount < 2) {
|
if (retryCount < 2) {
|
||||||
@@ -1042,8 +1042,7 @@ class NavigationController extends GetxController
|
|||||||
|
|
||||||
if (routeSteps.isNotEmpty) {
|
if (routeSteps.isNotEmpty) {
|
||||||
currentInstruction = routeSteps[0]['text'] ?? "";
|
currentInstruction = routeSteps[0]['text'] ?? "";
|
||||||
currentManeuverModifier =
|
currentManeuverModifier = ManeuverSign.fromValue(routeSteps[0]['sign']);
|
||||||
ManeuverSign.fromValue(routeSteps[0]['sign']);
|
|
||||||
nextInstruction = routeSteps.length > 1
|
nextInstruction = routeSteps.length > 1
|
||||||
? (langCode == 'ar'
|
? (langCode == 'ar'
|
||||||
? "ثم ${routeSteps[1]['text']}"
|
? "ثم ${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