diff --git a/siro_rider/assets/images/A.png b/siro_rider/assets/images/A.png index 246b9d21..66080e2c 100644 Binary files a/siro_rider/assets/images/A.png and b/siro_rider/assets/images/A.png differ diff --git a/siro_rider/assets/images/b.png b/siro_rider/assets/images/b.png index b22f96cc..1798e2cb 100644 Binary files a/siro_rider/assets/images/b.png and b/siro_rider/assets/images/b.png differ diff --git a/siro_rider/assets/images/car.png b/siro_rider/assets/images/car.png index 93c2e27d..8dff5920 100644 Binary files a/siro_rider/assets/images/car.png and b/siro_rider/assets/images/car.png differ diff --git a/siro_rider/lib/controller/home/map/map_engine_controller.dart b/siro_rider/lib/controller/home/map/map_engine_controller.dart index eba4181e..d31f3d6c 100644 --- a/siro_rider/lib/controller/home/map/map_engine_controller.dart +++ b/siro_rider/lib/controller/home/map/map_engine_controller.dart @@ -92,7 +92,7 @@ class MapEngineController extends GetxController { update(); } - void onStyleLoaded() async { + Future onStyleLoaded() async { Log.print('🗺️ Siro Map Style Loaded. Initializing...'); isStyleLoaded = true; await _loadMapIcons(); @@ -207,7 +207,12 @@ class MapEngineController extends GetxController { } int? _getImageSize(String id) { - if (id == carIcon || id == motoIcon || id == ladyIcon) return 120; + if (id == carIcon || id == motoIcon || id == ladyIcon) { + // Scale based on pixel ratio so it's not tiny on iOS Retina displays + double ratio = ui.window.devicePixelRatio; + if (ratio == 0) ratio = 3.0; // fallback just in case + return (120 * ratio).toInt(); + } return null; } diff --git a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart index 95f9bff6..ea65b0ba 100644 --- a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart +++ b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart @@ -3719,18 +3719,18 @@ class RideLifecycleController extends GetxController { // بناء الـ markers أولاً ثم وضعها معاً مع polylines في تحديث واحد final Set newMarkers = { + // لا نمرر InfoWindow هنا: صور A/B تحتوي الحرف أصلاً، وأي textField + // إضافي يرسم حرفاً مكرراً فوق الدبوس بعد إصلاح الخطوط في الـ SDK. Marker( markerId: const MarkerId('start'), position: startLoc, - icon: InlqBitmap.fromAsset('assets/images/A.png'), - infoWindow: const InfoWindow(title: 'A'), + icon: InlqBitmap.fromStyleImage('start_icon'), anchor: const Offset(0.5, 1.0), ), Marker( markerId: const MarkerId('end'), position: endLoc, - icon: InlqBitmap.fromAsset('assets/images/b.png'), - infoWindow: const InfoWindow(title: 'B'), + icon: InlqBitmap.fromStyleImage('end_icon'), anchor: const Offset(0.5, 1.0), ), }; @@ -3742,11 +3742,9 @@ class RideLifecycleController extends GetxController { newMarkers.add(Marker( markerId: MarkerId('waypoint_$i'), position: wp, - icon: InlqBitmap.fromAsset(isFirstWaypoint - ? 'assets/images/A.png' - : 'assets/images/b.png'), - infoWindow: - InfoWindow(title: isFirstWaypoint ? 'Stop 1' : 'Stop 2'), + icon: InlqBitmap.fromStyleImage(isFirstWaypoint + ? 'start_icon' + : 'end_icon'), anchor: const Offset(0.5, 1.0), )); } @@ -3758,27 +3756,12 @@ class RideLifecycleController extends GetxController { Log.print( '✅ FIX P1 v2: ${newMarkers.length} markers placed — start: $startLoc, end: $endLoc'); - // إضافة الـ markers مباشرة عبر الـ controller لضمان ظهورها - try { - final ctrl = mapEngine.mapController; - if (ctrl != null) { - for (final m in newMarkers) { - await ctrl.addMarker(m); - } - Log.print( - '✅ Added ${newMarkers.length} markers via controller.addMarker()'); - } else { - Log.print('⚠️ mapController is null, relying on declarative markers'); - } - } catch (e) { - Log.print('⚠️ addMarker via controller failed: $e'); - } - await bottomSheet(); await mapEngine.playRouteAnimation( mapEngine.polylineCoordinates, mapEngine.lastComputedBounds); + mapEngine.update(); // Make sure the map UI rebuilds with the new markers update(); } catch (e, stackTrace) { if (isDrawingRoute) { diff --git a/siro_rider/lib/views/home/map_page_passenger.dart b/siro_rider/lib/views/home/map_page_passenger.dart index a1c0eec7..3f5c9689 100644 --- a/siro_rider/lib/views/home/map_page_passenger.dart +++ b/siro_rider/lib/views/home/map_page_passenger.dart @@ -20,7 +20,7 @@ import 'map_widget.dart/car_details_widget_to_go.dart'; import 'map_widget.dart/cash_confirm_bottom_page.dart'; import 'map_widget.dart/google_map_passenger_widget.dart'; import 'map_widget.dart/left_main_menu_icons.dart'; -import 'map_widget.dart/main_bottom_menu_map.dart'; +import 'route_planner/route_planner.dart'; import 'map_widget.dart/map_menu_widget.dart'; import '../../controller/functions/package_info.dart'; import 'map_widget.dart/passengerRideLoctionWidget.dart'; @@ -65,9 +65,9 @@ class MapPagePassenger extends StatelessWidget { // OsmMapPassengerWidget(), leftMainMenuIcons(), // PaymobPackage(), - const PickerIconOnMap(), + const RpCenterPin(), // PickerAnimtionContainerFormPlaces(), - const MainBottomMenuMap(), + const RoutePlannerSheet(), // NewMainBottomSheet(), buttomSheetMapPage(), @@ -200,27 +200,4 @@ class CancelRidePageShow extends StatelessWidget { } } -class PickerIconOnMap extends StatelessWidget { - const PickerIconOnMap({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return GetBuilder( - builder: (controller) => controller.isPickerShown - ? Positioned( - bottom: Get.height * .2, - top: 0, - left: 0, - right: 0, - child: controller.isPickerShown - ? const Icon( - Icons.add_location, - color: Colors.purple, - ) - : const SizedBox(), - ) - : const SizedBox()); - } -} +// PickerIconOnMap was replaced by RpCenterPin (see route_planner/rp_center_pin.dart). diff --git a/siro_rider/lib/views/home/map_widget.dart/form_search_places_destenation.dart b/siro_rider/lib/views/home/map_widget.dart/form_search_places_destenation.dart index 224323b0..7b11e719 100644 --- a/siro_rider/lib/views/home/map_widget.dart/form_search_places_destenation.dart +++ b/siro_rider/lib/views/home/map_widget.dart/form_search_places_destenation.dart @@ -7,7 +7,6 @@ import 'package:get/get.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; import 'package:siro_rider/constant/box_name.dart'; import 'package:siro_rider/constant/table_names.dart'; -import 'package:siro_rider/views/widgets/elevated_btn.dart'; import '../../../constant/colors.dart'; import '../../../constant/style.dart'; @@ -15,7 +14,6 @@ import '../../../controller/functions/toast.dart'; import '../../../controller/home/map/location_search_controller.dart'; import '../../../controller/home/map/map_engine_controller.dart'; import '../../../controller/home/map/ride_lifecycle_controller.dart'; -import '../../../controller/home/map/ride_state.dart'; import '../../../main.dart'; // --------------------------------------------------- @@ -119,55 +117,94 @@ class _SearchFieldState extends State<_SearchField> { @override Widget build(BuildContext context) { + const Color accent = Color(0xFFEF4444); // matches the red destination dot return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Row( - children: [ - Expanded( - child: TextFormField( - controller: widget.controller.placeDestinationController, - onChanged: _onSearchChanged, - decoration: InputDecoration( - hintText: widget.controller.hintTextDestinationPoint, - hintStyle: AppStyle.subtitle.copyWith(color: Colors.grey[600]), - prefixIcon: Icon(Icons.search, color: AppColor.primaryColor), - suffixIcon: widget - .controller.placeDestinationController.text.isNotEmpty - ? IconButton( - icon: Icon(Icons.clear, color: Colors.grey[400]), - onPressed: () { - widget.controller.placeDestinationController.clear(); - }, - ) - : null, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16.0, vertical: 10.0), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.0), - borderSide: BorderSide.none, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + gradient: LinearGradient(colors: [ + accent.withOpacity(0.08), + accent.withOpacity(0.03), + ]), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: accent.withOpacity(0.35), width: 1.4), + ), + child: Row( + children: [ + Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: accent, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: accent.withOpacity(0.4), + blurRadius: 10, + offset: const Offset(0, 3)), + ], + ), + child: + const Icon(Icons.flag_rounded, color: Colors.white, size: 17), + ), + const SizedBox(width: 10), + Expanded( + child: TextFormField( + controller: widget.controller.placeDestinationController, + onChanged: _onSearchChanged, + style: const TextStyle( + fontSize: 14.5, fontWeight: FontWeight.w700), + decoration: InputDecoration( + hintText: widget.controller.hintTextDestinationPoint, + hintStyle: AppStyle.subtitle.copyWith( + color: accent.withOpacity(0.85), + fontWeight: FontWeight.w700, + fontSize: 14.5, + ), + isDense: true, + suffixIcon: widget + .controller.placeDestinationController.text.isNotEmpty + ? IconButton( + icon: Icon(Icons.clear, color: Colors.grey[400]), + onPressed: () { + widget.controller.placeDestinationController + .clear(); + }, + ) + : null, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + focusedBorder: InputBorder.none, + filled: false, ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.0), - borderSide: BorderSide(color: AppColor.primaryColor), - ), - filled: true, - fillColor: Colors.grey[50], ), ), - ), - const SizedBox(width: 8.0), - IconButton( - onPressed: () { - widget.mapEngine.changeMainBottomMenuMap(); - widget.mapEngine.changePickerShown(); - }, - icon: Icon(Icons.location_on_outlined, - color: AppColor.accentColor, size: 30), - tooltip: widget.rideLifecycle.isAnotherOreder - ? 'Pick destination on map'.tr - : 'Pick on map'.tr, - ), - ], + const SizedBox(width: 6.0), + InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + widget.mapEngine.changeMainBottomMenuMap(); + widget.mapEngine.changePickerShown(); + }, + child: Tooltip( + message: widget.rideLifecycle.isAnotherOreder + ? 'Pick destination on map'.tr + : 'Pick on map'.tr, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: accent.withOpacity(0.3)), + ), + child: Icon(Icons.map_rounded, color: accent, size: 18), + ), + ), + ), + ], + ), ), ); } diff --git a/siro_rider/lib/views/home/map_widget.dart/form_search_start.dart b/siro_rider/lib/views/home/map_widget.dart/form_search_start.dart index 4fd7b058..07f079b2 100644 --- a/siro_rider/lib/views/home/map_widget.dart/form_search_start.dart +++ b/siro_rider/lib/views/home/map_widget.dart/form_search_start.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; -import '../../../constant/colors.dart'; import '../../../constant/style.dart'; import '../../../controller/home/map/location_search_controller.dart'; import '../../../controller/home/map/map_engine_controller.dart'; @@ -16,64 +15,104 @@ GetBuilder formSearchPlacesStart() { id: 'start_point_form', builder: (controller) { final mapEngine = Get.find(); + const Color accent = Color(0xFF16A34A); // green: "their" pickup point return Column( children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Row( - children: [ - Expanded( - child: TextFormField( - controller: controller.placeStartController, - onChanged: (value) { - if (controller.placeStartController.text.length > 2) { - controller.getPlacesStart(); - } else if (controller.placeStartController.text.isEmpty) { - controller.clearPlacesStart(); - } - }, - decoration: InputDecoration( - hintText: 'Search for a starting point'.tr, - hintStyle: - AppStyle.subtitle.copyWith(color: Colors.grey[600]), - prefixIcon: - Icon(Icons.search, color: AppColor.primaryColor), - suffixIcon: controller.placeStartController.text.isNotEmpty - ? IconButton( - icon: Icon(Icons.clear, color: Colors.grey[400]), - onPressed: () { - controller.placeStartController.clear(); - controller.clearPlacesStart(); - }, - ) - : null, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16.0, vertical: 10.0), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.0), - borderSide: BorderSide.none, + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + gradient: LinearGradient(colors: [ + accent.withOpacity(0.08), + accent.withOpacity(0.03), + ]), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: accent.withOpacity(0.35), width: 1.4), + ), + child: Row( + children: [ + Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: accent, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: accent.withOpacity(0.4), + blurRadius: 10, + offset: const Offset(0, 3)), + ], + ), + child: const Icon(Icons.person_pin_circle_rounded, + color: Colors.white, size: 18), + ), + const SizedBox(width: 10), + Expanded( + child: TextFormField( + controller: controller.placeStartController, + style: const TextStyle( + fontSize: 14.5, fontWeight: FontWeight.w700), + onChanged: (value) { + if (controller.placeStartController.text.length > 2) { + controller.getPlacesStart(); + } else if (controller + .placeStartController.text.isEmpty) { + controller.clearPlacesStart(); + } + }, + decoration: InputDecoration( + hintText: 'Search for a starting point'.tr, + hintStyle: AppStyle.subtitle.copyWith( + color: accent.withOpacity(0.85), + fontWeight: FontWeight.w700, + fontSize: 14.5, + ), + isDense: true, + suffixIcon: controller + .placeStartController.text.isNotEmpty + ? IconButton( + icon: + Icon(Icons.clear, color: Colors.grey[400]), + onPressed: () { + controller.placeStartController.clear(); + controller.clearPlacesStart(); + }, + ) + : null, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + focusedBorder: InputBorder.none, + filled: false, ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.0), - borderSide: BorderSide(color: AppColor.primaryColor), - ), - filled: true, - fillColor: Colors.grey[50], ), ), - ), - const SizedBox(width: 8.0), - IconButton( - onPressed: () { - controller.passengerStartLocationFromMap = true; - mapEngine.changeMainBottomMenuMap(); - mapEngine.changePickerShown(); - }, - icon: Icon(Icons.location_on_outlined, - color: AppColor.accentColor, size: 30), - tooltip: 'Pick start point on map'.tr, - ), - ], + const SizedBox(width: 6.0), + InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () { + controller.passengerStartLocationFromMap = true; + mapEngine.changeMainBottomMenuMap(); + mapEngine.changePickerShown(); + }, + child: Tooltip( + message: 'Pick start point on map'.tr, + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: accent.withOpacity(0.3)), + ), + child: Icon(Icons.map_rounded, color: accent, size: 18), + ), + ), + ), + ], + ), ), ), AnimatedContainer( @@ -96,7 +135,8 @@ GetBuilder formSearchPlacesStart() { var address = res['address'] ?? 'Details not available'; return ListTile( - leading: const Icon(Icons.place, size: 30, color: Colors.grey), + leading: + const Icon(Icons.place, size: 30, color: Colors.grey), title: Text(title, style: AppStyle.subtitle .copyWith(fontWeight: FontWeight.w500)), @@ -106,8 +146,8 @@ GetBuilder formSearchPlacesStart() { var latitude = res['latitude']; var longitude = res['longitude']; if (latitude != null && longitude != null) { - controller.passengerLocation = - LatLng(double.parse(latitude), double.parse(longitude)); + controller.passengerLocation = LatLng( + double.parse(latitude), double.parse(longitude)); controller.placeStartController.text = title; controller.clearPlacesStart(); mapEngine.changeMainBottomMenuMap(); diff --git a/siro_rider/lib/views/home/map_widget.dart/main_bottom_Menu_map.dart b/siro_rider/lib/views/home/map_widget.dart/main_bottom_Menu_map.dart index da7f9520..501a86fa 100644 --- a/siro_rider/lib/views/home/map_widget.dart/main_bottom_Menu_map.dart +++ b/siro_rider/lib/views/home/map_widget.dart/main_bottom_Menu_map.dart @@ -1,7 +1,6 @@ import 'dart:ui' show ImageFilter; import 'package:siro_rider/print.dart'; import 'package:siro_rider/views/widgets/my_textField.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:siro_rider/constant/box_name.dart'; @@ -74,7 +73,6 @@ class _D { ], stops: const [0.0, 0.5, 1.0], ); - } // ───────────────────────────────────────────────────────────────────────────── @@ -1050,65 +1048,91 @@ class _OrderTypeButton extends StatelessWidget { Widget build(BuildContext context) { final rideLifecycle = Get.find(); final bool isOther = mapEngine.isAnotherOreder; - final Color accent = - isOther ? Colors.indigo.shade500 : AppColor.primaryColor; + const MaterialColor accent = Colors.indigo; - return Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - showCupertinoModalPopup( - context: context, - builder: (ctx) => CupertinoActionSheet( - title: Text('Select Order Type'.tr), - actions: [ - CupertinoActionSheetAction( - child: Text('I want to order for myself'.tr), - onPressed: () { - mapEngine.changeisAnotherOreder(false); - rideLifecycle.isAnotherOreder = false; - Navigator.pop(ctx); - }, - ), - CupertinoActionSheetAction( - child: Text('I want to order for someone else'.tr), - onPressed: () { - mapEngine.changeisAnotherOreder(true); - rideLifecycle.isAnotherOreder = true; - Navigator.pop(ctx); - }, - ), - ], - cancelButton: CupertinoActionSheetAction( - isDefaultAction: true, - onPressed: () => Navigator.pop(ctx), - child: Text('Cancel'.tr)), - ), - ); - }, - borderRadius: BorderRadius.circular(_D.radiusInner), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - decoration: BoxDecoration( - color: accent.withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(_D.radiusInner)), - child: Row( - children: [ - Icon(isOther ? Icons.person_rounded : Icons.group_rounded, - color: accent, size: 17), - const SizedBox(width: 14), - Expanded( - child: Text( - isOther - ? 'Order for myself'.tr - : 'Order for someone else'.tr, + void select(bool other) { + if (mapEngine.isAnotherOreder == other) return; + mapEngine.changeisAnotherOreder(other); + rideLifecycle.isAnotherOreder = other; + } + + Widget segment( + {required bool selected, + required IconData icon, + required String label, + required VoidCallback onTap}) { + return Expanded( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(_D.radiusInner - 2), + child: AnimatedContainer( + duration: _D.fast, + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: selected ? accent.shade500 : Colors.transparent, + borderRadius: BorderRadius.circular(_D.radiusInner - 2), + boxShadow: selected + ? [ + BoxShadow( + color: accent.withValues(alpha: 0.35), + blurRadius: 10, + offset: const Offset(0, 3)), + ] + : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, + color: selected ? Colors.white : Colors.grey.shade500, + size: 16), + const SizedBox(width: 8), + Flexible( + child: Text( + label, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: TextStyle( - color: accent, - fontSize: 13.5, - fontWeight: FontWeight.w600))), - ], + color: selected ? Colors.white : Colors.grey.shade600, + fontSize: 13, + fontWeight: + selected ? FontWeight.w700 : FontWeight.w500, + ), + ), + ), + ], + ), + ), ), ), + ); + } + + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(_D.radiusInner), + ), + child: Row( + children: [ + segment( + selected: !isOther, + icon: Icons.person_rounded, + label: 'Order for myself'.tr, + onTap: () => select(false), + ), + const SizedBox(width: 4), + segment( + selected: isOther, + icon: Icons.group_rounded, + label: 'Order for someone else'.tr, + onTap: () => select(true), + ), + ], ), ); } diff --git a/siro_rider/lib/views/home/navigation/navigation_controller.dart b/siro_rider/lib/views/home/navigation/navigation_controller.dart index 75a297f7..b19ed4a0 100644 --- a/siro_rider/lib/views/home/navigation/navigation_controller.dart +++ b/siro_rider/lib/views/home/navigation/navigation_controller.dart @@ -946,9 +946,9 @@ class NavigationController extends GetxController Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams); try { - final response = await http - .get(saasUri, headers: {'x-api-key': Env.mapSaasKey}) - .timeout(const Duration(seconds: 15)); + final response = await http.get(saasUri, headers: { + 'x-api-key': Env.mapSaasKey + }).timeout(const Duration(seconds: 15)); if (response.statusCode != 200) { if (retryCount < 2) { @@ -1042,8 +1042,7 @@ class NavigationController extends GetxController if (routeSteps.isNotEmpty) { currentInstruction = routeSteps[0]['text'] ?? ""; - currentManeuverModifier = - ManeuverSign.fromValue(routeSteps[0]['sign']); + currentManeuverModifier = ManeuverSign.fromValue(routeSteps[0]['sign']); nextInstruction = routeSteps.length > 1 ? (langCode == 'ar' ? "ثم ${routeSteps[1]['text']}" diff --git a/siro_rider/lib/views/home/route_planner/route_planner.dart b/siro_rider/lib/views/home/route_planner/route_planner.dart new file mode 100644 index 00000000..a3154986 --- /dev/null +++ b/siro_rider/lib/views/home/route_planner/route_planner.dart @@ -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'; diff --git a/siro_rider/lib/views/home/route_planner/rp_actions.dart b/siro_rider/lib/views/home/route_planner/rp_actions.dart new file mode 100644 index 00000000..c802fae0 --- /dev/null +++ b/siro_rider/lib/views/home/route_planner/rp_actions.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(); + static MapEngineController get _map => Get.find(); + static RideLifecycleController get _ride => + Get.find(); + + // ── Destination ───────────────────────────────────────────────────────── + static void pickDestinationOnMap() { + _map.changeMainBottomMenuMap(); + _map.changePickerShown(); + } + + static Future 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 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 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 goToStoredPlace(dynamic place) async { + await _s.getLocation(); + await _ride.getDirectionMap( + '${_s.passengerLocation.latitude},${_s.passengerLocation.longitude}', + '${place['latitude']},${place['longitude']}', + ); + _ride.showBottomSheet1(); + } +} diff --git a/siro_rider/lib/views/home/route_planner/rp_center_pin.dart b/siro_rider/lib/views/home/route_planner/rp_center_pin.dart new file mode 100644 index 00000000..a758a2f3 --- /dev/null +++ b/siro_rider/lib/views/home/route_planner/rp_center_pin.dart @@ -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 createState() => _RpCenterPinState(); +} + +class _RpCenterPinState extends State + 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( + builder: (mapEngine) { + if (!mapEngine.isPickerShown) return const SizedBox.shrink(); + + return GetBuilder( + 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; +} diff --git a/siro_rider/lib/views/home/route_planner/rp_favorites.dart b/siro_rider/lib/views/home/route_planner/rp_favorites.dart new file mode 100644 index 00000000..40b25f4f --- /dev/null +++ b/siro_rider/lib/views/home/route_planner/rp_favorites.dart @@ -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), + ), + ], + ), + ), + ), + ); + } +} diff --git a/siro_rider/lib/views/home/route_planner/rp_location_field.dart b/siro_rider/lib/views/home/route_planner/rp_location_field.dart new file mode 100644 index 00000000..e8a3224f --- /dev/null +++ b/siro_rider/lib/views/home/route_planner/rp_location_field.dart @@ -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 createState() => _RpLocationFieldState(); +} + +class _RpLocationFieldState extends State { + 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); + } +} diff --git a/siro_rider/lib/views/home/route_planner/rp_map_pick_overlay.dart b/siro_rider/lib/views/home/route_planner/rp_map_pick_overlay.dart new file mode 100644 index 00000000..2fdfde40 --- /dev/null +++ b/siro_rider/lib/views/home/route_planner/rp_map_pick_overlay.dart @@ -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(); + 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(); + + return GetBuilder( + 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 _confirm(BuildContext context, MapEngineController mapEngine, + LocationSearchController s) async { + final rideLifecycle = Get.find(); + 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, + ), + ), + ), + ); + } +} diff --git a/siro_rider/lib/views/home/route_planner/rp_search_results.dart b/siro_rider/lib/views/home/route_planner/rp_search_results.dart new file mode 100644 index 00000000..accf8c59 --- /dev/null +++ b/siro_rider/lib/views/home/route_planner/rp_search_results.dart @@ -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), + ), + ], + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/siro_rider/lib/views/home/route_planner/rp_sheet.dart b/siro_rider/lib/views/home/route_planner/rp_sheet.dart new file mode 100644 index 00000000..d5bece12 --- /dev/null +++ b/siro_rider/lib/views/home/route_planner/rp_sheet.dart @@ -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( + 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(); + final rideLifecycle = Get.find(); + + return GetBuilder( + 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(); + + return GetBuilder( + builder: (s) { + final rideLifecycle = Get.find(); + 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(); + 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(); + final rideLifecycle = Get.find(); + + 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().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), + ), + ); + } +} diff --git a/siro_rider/lib/views/home/route_planner/rp_theme.dart b/siro_rider/lib/views/home/route_planner/rp_theme.dart new file mode 100644 index 00000000..b859811d --- /dev/null +++ b/siro_rider/lib/views/home/route_planner/rp_theme.dart @@ -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 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 get cardShadow => [ + BoxShadow( + color: Colors.black.withValues(alpha: isDark ? 0.35 : 0.08), + blurRadius: 24, + spreadRadius: -8, + offset: const Offset(0, 8), + ), + ]; + + static List 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), + ), + ); +} diff --git a/siro_rider/route.json b/siro_rider/route.json new file mode 100644 index 00000000..d6c5796a --- /dev/null +++ b/siro_rider/route.json @@ -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":[]} \ No newline at end of file