Files
Siro/siro_rider/lib/views/home/route_planner/rp_sheet.dart
T

723 lines
25 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
import '../../../controller/home/map/location_search_controller.dart';
import '../../../controller/home/map/map_engine_controller.dart';
import '../../../controller/home/map/ride_lifecycle_controller.dart';
import '../../../main.dart';
import '../../widgets/elevated_btn.dart';
import '../../widgets/my_textField.dart';
import 'rp_actions.dart';
import 'rp_favorites.dart';
import 'rp_location_field.dart';
import 'rp_map_pick_overlay.dart';
import 'rp_theme.dart';
/// The route-planning surface anchored to the bottom of the map.
///
/// • While the user drags the map to place a point → [RpMapPickOverlay].
/// • Otherwise → a collapsed "Where to?" card or the full planner.
///
/// Drop-in replacement for the old `MainBottomMenuMap`. Uses only the
/// existing controller APIs; nothing in the controllers changed.
class RoutePlannerSheet extends StatelessWidget {
const RoutePlannerSheet({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<MapEngineController>(
builder: (mapEngine) {
if (mapEngine.isPickerShown) return const RpMapPickOverlay();
return Positioned(
left: 14,
right: 14,
bottom: Get.height * .03,
child: AnimatedContainer(
duration: RP.medium,
curve: RP.ease,
constraints: BoxConstraints(
maxHeight: mapEngine.isMainBottomMenuMap
? Get.height * 0.42
: Get.height * 0.72,
),
decoration: BoxDecoration(
color: RP.sheet,
borderRadius: BorderRadius.circular(RP.rSheet),
boxShadow: RP.sheetShadow,
border: Border.all(color: RP.divider),
),
clipBehavior: Clip.antiAlias,
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: mapEngine.isMainBottomMenuMap
? const _Collapsed()
: const _Expanded(),
),
),
);
},
);
}
}
// ══════════════════════════════════════════════════════════════════════════
// COLLAPSED
// ══════════════════════════════════════════════════════════════════════════
class _Collapsed extends StatelessWidget {
const _Collapsed();
@override
Widget build(BuildContext context) {
final String firstName =
(box.read(BoxName.name)?.toString().split(' ').first ?? '').trim();
final mapEngine = Get.find<MapEngineController>();
final rideLifecycle = Get.find<RideLifecycleController>();
return GetBuilder<LocationSearchController>(
builder: (locationSearch) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 12),
RP.grabber(),
const SizedBox(height: 14),
InkWell(
onTap: mapEngine.changeMainBottomMenuMap,
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: RP.brand,
borderRadius: BorderRadius.circular(16),
boxShadow: RP.glow(RP.brand),
),
child: const Icon(Icons.search_rounded,
color: Colors.white, size: 24),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text.rich(
TextSpan(children: [
TextSpan(
text: '${'Where to'.tr} ',
style: TextStyle(
fontSize: 16.5,
fontWeight: FontWeight.w700,
color: RP.textStrong),
),
if (firstName.isNotEmpty)
TextSpan(
text: firstName,
style: TextStyle(
fontSize: 16.5,
fontWeight: FontWeight.w800,
color: RP.brand),
),
const TextSpan(text: '؟'),
]),
),
if (!rideLifecycle.noCarString) ...[
const SizedBox(height: 2),
Text('Tap to search your destination'.tr,
style:
TextStyle(fontSize: 12, color: RP.textMuted)),
],
],
),
),
Icon(Icons.keyboard_arrow_up_rounded,
color: RP.textMuted, size: 26),
],
),
),
),
const SizedBox(height: 14),
RpRecentsRow(locationSearch: locationSearch),
const SizedBox(height: 16),
],
);
},
);
}
}
// ══════════════════════════════════════════════════════════════════════════
// EXPANDED
// ══════════════════════════════════════════════════════════════════════════
class _Expanded extends StatelessWidget {
const _Expanded();
@override
Widget build(BuildContext context) {
final mapEngine = Get.find<MapEngineController>();
return GetBuilder<LocationSearchController>(
builder: (s) {
final rideLifecycle = Get.find<RideLifecycleController>();
final bool isOther = rideLifecycle.isAnotherOreder;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 12),
Center(child: RP.grabber()),
// Header
Padding(
padding: const EdgeInsets.fromLTRB(20, 14, 14, 6),
child: Row(
children: [
Text('Plan Your Route'.tr, style: RP.title),
const Spacer(),
_CircleIconButton(
icon: Icons.keyboard_arrow_down_rounded,
onTap: mapEngine.changeMainBottomMenuMap,
),
],
),
),
// Order-type toggle
Padding(
padding: const EdgeInsets.fromLTRB(16, 6, 16, 4),
child: _OrderTypeToggle(isOther: isOther),
),
// Route timeline
Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 16, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_originField(s, isOther),
const SizedBox(height: 10),
...List.generate(
s.activeMenuWaypointCount,
(i) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _StopRow(index: i, s: s),
),
),
if (s.activeMenuWaypointCount < 2) ...[
_AddStopButton(onTap: RpActions.addStop),
const SizedBox(height: 10),
],
_destinationField(s),
],
),
),
const SizedBox(height: 6),
_Divider(),
// Quick access
_SectionHeader(
title: 'Quick Access'.tr,
trailing: const RpFavoritesButton(),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(child: _homeWorkButton(isWork: false)),
const SizedBox(width: 12),
Expanded(child: _homeWorkButton(isWork: true)),
],
),
),
const SizedBox(height: 14),
RpRecentsRow(locationSearch: s),
const SizedBox(height: 6),
_Divider(),
// Advanced
_SectionHeader(title: 'Advanced Tools'.tr),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _WhatsAppLinkButton(locationSearch: s),
),
const SizedBox(height: 22),
],
);
},
);
}
// ── Origin ──────────────────────────────────────────────────────────────
Widget _originField(LocationSearchController s, bool isOther) {
if (isOther) {
return RpLocationField(
accent: RP.origin,
icon: Icons.person_pin_circle_rounded,
controller: s.placeStartController,
hintText: 'Search for a starting point'.tr,
results: s.placesStart,
onQuery: (_) => s.getPlacesStart(),
onEmpty: s.clearPlacesStart,
onPickOnMap: RpActions.pickOtherPickupOnMap,
onResultTap: (i) => RpActions.selectStart(s.placesStart[i]),
);
}
// Regular order: origin = current location, changeable on the map.
return RpLocationField(
accent: RP.origin,
icon: Icons.my_location_rounded,
controller: s.placeStartController,
hintText: s.currentLocationString,
results: const [],
searchable: false,
onQuery: (_) {},
onEmpty: () {},
onPickOnMap: RpActions.pickMyStartOnMap,
onResultTap: (_) {},
);
}
// ── Destination ─────────────────────────────────────────────────────────
Widget _destinationField(LocationSearchController s) {
final mapEngine = Get.find<MapEngineController>();
return RpLocationField(
accent: RP.destination,
icon: Icons.flag_rounded,
controller: s.placeDestinationController,
hintText: s.hintTextDestinationPoint,
results: s.placesDestination,
onQuery: (_) {
s.getPlaces();
mapEngine.changeHeightPlaces();
},
onEmpty: () {
s.clearPlacesDestination();
mapEngine.changeHeightPlaces();
},
onPickOnMap: RpActions.pickDestinationOnMap,
onResultTap: (i) => RpActions.selectDestination(i, s.placesDestination[i]),
onResultFavorite: (i) {
final res = s.placesDestination[i];
RpActions.addFavorite(
Get.context!,
res['latitude'],
res['longitude'],
(res['name_ar'] ?? res['name'] ?? 'Unknown Place').toString(),
);
},
);
}
Widget _homeWorkButton({required bool isWork}) {
final String saved = isWork
? (box.read(BoxName.addWork)?.toString() ?? 'addWork')
: (box.read(BoxName.addHome)?.toString() ?? 'addHome');
final bool isSet = isWork ? saved != 'addWork' : saved != 'addHome';
final Color accent =
isWork ? const Color(0xFF2563EB) : const Color(0xFFF59E0B);
return _QuickTile(
icon: isWork ? Icons.work_rounded : Icons.home_rounded,
accent: accent,
label: isWork
? (isSet ? 'To Work'.tr : 'Add Work'.tr)
: (isSet ? 'To Home'.tr : 'Add Home'.tr),
onTap: () {
if (isSet) {
RpActions.goToSaved(
isWork ? BoxName.addWork : BoxName.addHome,
isWork ? 'To Work' : 'To Home');
} else {
RpActions.pickHomeOrWork(isWork: isWork);
}
},
onLongPress: () => RpActions.changeHomeOrWork(isWork: isWork),
);
}
}
// ── Stop row ────────────────────────────────────────────────────────────────
class _StopRow extends StatelessWidget {
final int index;
final LocationSearchController s;
const _StopRow({required this.index, required this.s});
@override
Widget build(BuildContext context) {
final Color accent = RP.stopColor(index);
final bool isSet = s.menuWaypoints[index] != null;
final String name =
isSet ? s.menuWaypointNames[index] : '${'Stop'.tr} ${index + 1}';
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: RP.fieldFill,
borderRadius: BorderRadius.circular(RP.rField),
border: Border.all(
color: isSet ? accent.withValues(alpha: 0.45) : Colors.transparent,
width: 1.4,
),
),
child: Row(
children: [
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: accent,
shape: BoxShape.circle,
boxShadow: RP.glow(accent),
),
child: Center(
child: Text('${index + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w800)),
),
),
const SizedBox(width: 10),
Expanded(
child: InkWell(
onTap: () => RpActions.pickStopOnMap(index),
child: Text(
name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: isSet
? RP.fieldValue
: RP.fieldHint,
),
),
),
InkWell(
onTap: () => RpActions.removeStop(index),
borderRadius: BorderRadius.circular(RP.rChip),
child: Container(
width: 34,
height: 34,
alignment: Alignment.center,
child: Icon(Icons.delete_outline_rounded,
color: RP.destination, size: 19),
),
),
InkWell(
onTap: () => RpActions.pickStopOnMap(index),
borderRadius: BorderRadius.circular(RP.rChip),
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(RP.rChip),
),
child: Icon(Icons.map_rounded, color: accent, size: 19),
),
),
],
),
);
}
}
class _AddStopButton extends StatelessWidget {
final VoidCallback onTap;
const _AddStopButton({required this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(RP.rField),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 13),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(RP.rField),
border: Border.all(
color: RP.stop1.withValues(alpha: 0.5),
width: 1.4,
style: BorderStyle.solid),
color: RP.stop1.withValues(alpha: 0.06),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add_location_alt_outlined,
color: RP.stop1, size: 18),
const SizedBox(width: 8),
Text('Add a Stop'.tr,
style: TextStyle(
color: RP.stop1,
fontSize: 13.5,
fontWeight: FontWeight.w700)),
],
),
),
);
}
}
// ── Order-type toggle ────────────────────────────────────────────────────────
class _OrderTypeToggle extends StatelessWidget {
final bool isOther;
const _OrderTypeToggle({required this.isOther});
@override
Widget build(BuildContext context) {
final mapEngine = Get.find<MapEngineController>();
final rideLifecycle = Get.find<RideLifecycleController>();
void select(bool other) {
if (mapEngine.isAnotherOreder == other) return;
mapEngine.changeisAnotherOreder(other);
rideLifecycle.isAnotherOreder = other;
// The expanded planner rebuilds on LocationSearchController updates, so
// refresh it here to swap the origin field (self ⇄ other) instantly.
Get.find<LocationSearchController>().update();
}
return Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: RP.chipFill,
borderRadius: BorderRadius.circular(RP.rField),
),
child: Row(
children: [
_seg(
selected: !isOther,
icon: Icons.person_rounded,
label: 'Order for myself'.tr,
onTap: () => select(false),
),
const SizedBox(width: 4),
_seg(
selected: isOther,
icon: Icons.group_rounded,
label: 'Order for someone else'.tr,
onTap: () => select(true),
),
],
),
);
}
Widget _seg({
required bool selected,
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return Expanded(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(RP.rChip),
child: AnimatedContainer(
duration: RP.fast,
padding: const EdgeInsets.symmetric(vertical: 11),
decoration: BoxDecoration(
color: selected ? const Color(0xFF4F46E5) : Colors.transparent,
borderRadius: BorderRadius.circular(RP.rChip),
boxShadow: selected
? RP.glow(const Color(0xFF4F46E5), intensity: 0.35)
: null,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon,
size: 16,
color: selected ? Colors.white : RP.textMuted),
const SizedBox(width: 7),
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12.5,
fontWeight:
selected ? FontWeight.w700 : FontWeight.w600,
color: selected ? Colors.white : RP.textBody,
),
),
),
],
),
),
),
),
);
}
}
// ── Quick tile (home/work) ───────────────────────────────────────────────────
class _QuickTile extends StatelessWidget {
final IconData icon;
final Color accent;
final String label;
final VoidCallback onTap;
final VoidCallback? onLongPress;
const _QuickTile({
required this.icon,
required this.accent,
required this.label,
required this.onTap,
this.onLongPress,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
onLongPress: onLongPress,
borderRadius: BorderRadius.circular(RP.rField),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
decoration: BoxDecoration(
color: accent.withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(RP.rField),
),
child: Row(
children: [
Icon(icon, color: accent, size: 20),
const SizedBox(width: 10),
Flexible(
child: Text(label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: accent,
fontSize: 13.5,
fontWeight: FontWeight.w700)),
),
],
),
),
);
}
}
// ── WhatsApp link ─────────────────────────────────────────────────────────────
class _WhatsAppLinkButton extends StatelessWidget {
final LocationSearchController locationSearch;
const _WhatsAppLinkButton({required this.locationSearch});
@override
Widget build(BuildContext context) {
return InkWell(
borderRadius: BorderRadius.circular(RP.rField),
onTap: () {
Get.dialog(
AlertDialog(
backgroundColor: RP.sheetElevated,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(RP.rCard)),
title: Text('WhatsApp Location Extractor'.tr, style: RP.title),
content: Form(
key: locationSearch.sosFormKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
MyTextForm(
controller: locationSearch.whatsAppLocationText,
label: 'Location Link'.tr,
type: TextInputType.url,
hint: 'https://maps.app.goo.gl/...'),
const SizedBox(height: 16),
MyElevatedButton(
title: 'Go to this location'.tr,
onPressed: () => locationSearch.goToWhatappLocation()),
],
),
),
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
decoration: BoxDecoration(
color: const Color(0xFF25D366).withValues(alpha: 0.10),
borderRadius: BorderRadius.circular(RP.rField),
),
child: Row(
children: [
const Icon(Icons.link_rounded,
color: Color(0xFF1FA855), size: 20),
const SizedBox(width: 12),
Expanded(
child: Text('Paste WhatsApp location link'.tr,
style: const TextStyle(
color: Color(0xFF1FA855),
fontSize: 13.5,
fontWeight: FontWeight.w700)),
),
],
),
),
);
}
}
// ── Small shared bits ─────────────────────────────────────────────────────────
class _SectionHeader extends StatelessWidget {
final String title;
final Widget? trailing;
const _SectionHeader({required this.title, this.trailing});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 4, 12, 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title.toUpperCase(), style: RP.sectionLabel),
if (trailing != null) trailing!,
],
),
);
}
}
class _Divider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 1,
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
color: RP.divider,
);
}
}
class _CircleIconButton extends StatelessWidget {
final IconData icon;
final VoidCallback onTap;
const _CircleIconButton({required this.icon, required this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(RP.rPill),
child: Container(
width: 38,
height: 38,
decoration: BoxDecoration(color: RP.fieldFill, shape: BoxShape.circle),
child: Icon(icon, size: 24, color: RP.textBody),
),
);
}
}