From 81d471566461bc68b01ecaae825489f9b71fa678 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 10 Jul 2026 16:09:37 +0300 Subject: [PATCH] Update: 2026-07-10 16:09:37 --- siro_rider/assets/images/A.png | Bin 6947 -> 2522 bytes siro_rider/assets/images/b.png | Bin 6809 -> 2327 bytes siro_rider/assets/images/car.png | Bin 4334 -> 5494 bytes .../home/map/map_engine_controller.dart | 9 +- .../home/map/ride_lifecycle_controller.dart | 33 +- .../lib/views/home/map_page_passenger.dart | 31 +- .../form_search_places_destenation.dart | 131 ++-- .../map_widget.dart/form_search_start.dart | 152 ++-- .../map_widget.dart/main_bottom_Menu_map.dart | 138 ++-- .../navigation/navigation_controller.dart | 9 +- .../home/route_planner/route_planner.dart | 10 + .../views/home/route_planner/rp_actions.dart | 187 +++++ .../home/route_planner/rp_center_pin.dart | 192 +++++ .../home/route_planner/rp_favorites.dart | 148 ++++ .../home/route_planner/rp_location_field.dart | 221 ++++++ .../route_planner/rp_map_pick_overlay.dart | 415 ++++++++++ .../home/route_planner/rp_search_results.dart | 123 +++ .../views/home/route_planner/rp_sheet.dart | 722 ++++++++++++++++++ .../views/home/route_planner/rp_theme.dart | 139 ++++ siro_rider/route.json | 1 + 20 files changed, 2442 insertions(+), 219 deletions(-) create mode 100644 siro_rider/lib/views/home/route_planner/route_planner.dart create mode 100644 siro_rider/lib/views/home/route_planner/rp_actions.dart create mode 100644 siro_rider/lib/views/home/route_planner/rp_center_pin.dart create mode 100644 siro_rider/lib/views/home/route_planner/rp_favorites.dart create mode 100644 siro_rider/lib/views/home/route_planner/rp_location_field.dart create mode 100644 siro_rider/lib/views/home/route_planner/rp_map_pick_overlay.dart create mode 100644 siro_rider/lib/views/home/route_planner/rp_search_results.dart create mode 100644 siro_rider/lib/views/home/route_planner/rp_sheet.dart create mode 100644 siro_rider/lib/views/home/route_planner/rp_theme.dart create mode 100644 siro_rider/route.json diff --git a/siro_rider/assets/images/A.png b/siro_rider/assets/images/A.png index 246b9d21ca997536c0ec1da7f4d6cb99b3b73f79..66080e2cdba801058a824a6aee82410dd9ba4374 100644 GIT binary patch literal 2522 zcmYjTdpr|*8@HMJXc%cjc37@0Y*WBqTzvy`|pCc|V```Fwu2@ArAW&+qsA{&}*Gdbukr!W5;Xq?CyS zthZ!Mmy8;DS;>qr!Ou%cDR7BcSIX(J#h`onZ?Mp(SAS>OD<@Zdmh)v(B5GFF^`hOI za0Vm;Z{}Md!c{{N3&4eo!jzO+DgtWbhp5L`$7^JrtkC{?8${B1<~frmKtrI0sAuqbuH1w9#+XWC#FQ8woUaw3@drT4kiBy{0^ zZ|bDWNjg-Mg-c!D+A#jIGE=|YcXj;R_y^q&-5*BkQ8ruWXPe_SUUiU(=A!&FfG2Pw?BApeH^+yr6(Gm_;}`@e0K`I~d#Uu3rqmXXm~_LZ{A zi~`KR!}3T1@r4kSKbPK8qp2YKJ|NXEkdhu!j$xoK1$9JY_M46JG z07oY7GUrUO+RbT`_*a1mHs?th=@FSvPT0Z#7Iv4Eo3qvKKZ7owBzh3O$j<#a$ZqAc z3<3SA!RWzoJt+2DHP0OLbFLgh zYVHj-$_@~7qwfyal4k{@J{&)LgSUnISj#o%31QT%sFyVrN=ApSURJFhs==^7GJD+j z##!;eaO$PGISdPBcTplkO|!|z{MZ^!DA4Ymc(}@`LQmViyT}BvIfE^h~PYB89mvNAhW)mV`KfReo&U`<-|t0 zO->$2F99aS6@Mp^RNZtzHz~eCXIIaYb?(HQn&VBtw78n;-mHO83NmwI6_k$GC*_cv z2`=GQJfRjXS>C~SneK=ZAepE<88*W^@6g?3z^InN;11nUV> z&>L2Bj1XrPS_vTkiS)GY6g$6)=*=p$6kx*~e3asA<`HK5$liD=PnX99fsTvWIDJxA zP2tT_MlIjKj{LKukt4jf{E^N`|D#)~;s^>NbGnK;01e z96LQj$HwfVD#(?bOX5N>PFBOgZJvG6#|$0I$G*%n74;SCAmmBFMIJw%8ZM4?sSE)#Ly8ov*Ci3bdS`0A+ONL%MdNCz7*Cf4k`o~`l-v-P$ zgBA5zJfn;*{a$MUq=Jp+)D8xZ!0bmAPY-=Km8)9fcaV!v4&>UVC)5}K3LFnfuhNC` z))4@jRMyAq^Hoou5S+AG%I;{*2S>pLJ{WBP=2P5lM)7Ig_cCm+dz{C!BskS=6grve zcmq9XU5(7_{}&9xQHt^yNeY$*S?2hfdRkDJXsiwr(Af^UCnTRzhR6%TeaC=tx0af- z>%9?;D`$JH_0{X@mqN$oS>0NlKS#baZXAy{-t%^#LEV_P$Fw$8we)w!=d(OeG-0tR zNU=Y;EyWqrU!aRXXV{~3wjGh{Bpu`*XP}46=Pm0Jn{GjjnNwz#e*z(2lWvZ7w<8(E z7)S2@4O0&b3ij~eKJ+SRSu;Y)Z7&pE20ZtkV9xO!238uPQm4$Wd0*SJ+%ni&#^;8U zd^(u#bKZl^=SGyJD_&lkv9D9;Yia=gR`-e?as#hN1IoTW{_Si1wpVCa>fP6MX6~Sj z*0p+eSkwi?gR$a~hgT1JCP)Gt@BOaB-x5bDneCC=l9)gQX^ACOvzixEKWrl{K>A8^=BF&{9Z;d=702 zU|?4TEjEd+cDKowt^Y7Mg@ZW8U#Y<}&}R`v7_XPQYauDO80orS0fETuj8@=l zzn+X`|z_A}tO8aG(7z1r0GuQ80PjNIaZmvp@sFaf<;$U_$;todGi#jlS z0QF$^LQg$udH-NWJ6M&Z0&yB8Xrb!6F7Y%+{HsbdR`LS$v6wQqU6CLfW1~0EaHFl1 zmL}@aegn<_G1j6L6!U3ogYO9dI=^AWx=zJr+58~MO)C^>^S);+f#x%>mIEYGt=O0| z*}E)HbSFu~Jk+0&(<}1=pPVYG0m`425TQRHL68sr>P~*K^CMpHBh;N5+efFb=e&8W wS)N18KO8;Sf5*`Asjx9)#%c2ZjBNi^+WDDi7JXd3OwzMS5piDFIt)GSzm6q$Qvd(} literal 6947 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 literal 6809 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 TNAWI~fBGmZXvn{jwFv$XK}*|f diff --git a/siro_rider/assets/images/car.png b/siro_rider/assets/images/car.png index 93c2e27d56499b51c145222294d5dc136205ed2f..8dff592040ce794137f8343b6ab40d84faa9d16c 100644 GIT binary patch literal 5494 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 literal 4334 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 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