MotorSignal-App/lib/features/routes/route_planner_screen.dart

1021 lines
41 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';
import 'package:go_router/go_router.dart';
import '../../services/auth/auth_providers.dart';
import '../../services/entitlements/entitlement_providers.dart';
import '../../services/map/map_provider_interface.dart';
import '../../services/map/unified_map_view.dart';
import '../../services/routes/navigation_handoff.dart';
import '../../services/routes/route_models.dart';
import '../../services/routes/route_providers.dart';
import '../../services/routes/route_service.dart';
class RoutePlannerScreen extends ConsumerStatefulWidget {
final String? initialRouteId;
final String? attachToGroupRideId;
const RoutePlannerScreen({
super.key,
this.initialRouteId,
this.attachToGroupRideId,
});
@override
ConsumerState<RoutePlannerScreen> createState() => _RoutePlannerScreenState();
}
class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
final _titleController = TextEditingController();
final _descController = TextEditingController();
RouteVisibility _visibility = RouteVisibility.private;
bool _initialized = false;
bool _isSaving = false;
String? _errorMessage;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadInitialData();
});
}
@override
void dispose() {
_titleController.dispose();
_descController.dispose();
super.dispose();
}
Future<void> _loadInitialData() async {
if (widget.initialRouteId != null) {
final service = ref.read(routeServiceProvider);
final route = await service.getRoute(widget.initialRouteId!);
if (route != null && mounted) {
ref.read(routePlannerNotifierProvider.notifier).loadRoute(route);
_titleController.text = route.title;
_descController.text = route.description;
setState(() {
_visibility = route.visibility;
_initialized = true;
});
return;
}
} else {
ref.read(routePlannerNotifierProvider.notifier).reset();
if (widget.attachToGroupRideId != null) {
setState(() {
_visibility = RouteVisibility.groupOnly;
});
}
}
setState(() {
_initialized = true;
});
}
Future<void> _addCurrentLocationStop() async {
try {
final position = await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
timeLimit: Duration(seconds: 10),
),
);
if (!mounted) return;
_showAddOrEditStopDialog(
initialLat: position.latitude,
initialLng: position.longitude,
defaultName: 'My Current Location',
);
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Could not get GPS location: $e'),
backgroundColor: Colors.redAccent,
),
);
}
}
void _showAddOrEditStopDialog({
RouteStop? existingStop,
double? initialLat,
double? initialLng,
String? defaultName,
}) {
final state = ref.read(routePlannerNotifierProvider);
final isPro = ref.read(isProSubscriberProvider);
final maxAllowed = isPro ? RouteService.maxTechnicalStops : RouteService.freeStopCap;
if (existingStop == null && state.stops.length >= maxAllowed) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
isPro
? 'Maximum of $maxAllowed stops reached.'
: 'Free tier limit reached ($maxAllowed stops). Upgrade to Pro for up to 25 stops.',
),
action: !isPro
? SnackBarAction(
label: 'UPGRADE',
textColor: const Color(0xFF00E5FF),
onPressed: () => context.push('/settings/pro'),
)
: null,
backgroundColor: const Color(0xFF1B1E26),
),
);
return;
}
final nameCtrl = TextEditingController(
text: existingStop?.displayName ?? defaultName ?? '',
);
final latCtrl = TextEditingController(
text: existingStop != null
? existingStop.latitude.toStringAsFixed(6)
: (initialLat?.toStringAsFixed(6) ?? ''),
);
final lngCtrl = TextEditingController(
text: existingStop != null
? existingStop.longitude.toStringAsFixed(6)
: (initialLng?.toStringAsFixed(6) ?? ''),
);
final notesCtrl = TextEditingController(text: existingStop?.notes ?? '');
RouteStopType selectedType = existingStop?.stopType ??
(state.stops.isEmpty
? RouteStopType.origin
: RouteStopType.waypoint);
showDialog(
context: context,
builder: (dialogCtx) => StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
backgroundColor: const Color(0xFF13151A),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Color(0xFF2A2E3D)),
),
title: Text(
existingStop != null ? 'Edit Stop' : 'Add Route Stop',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: nameCtrl,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
labelText: 'Stop Name *',
labelStyle: TextStyle(color: Colors.white60),
hintText: 'e.g. Skyline Summit, Fuel Stop',
hintStyle: TextStyle(color: Colors.white24),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFF2A2E3D)),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFF00E5FF)),
),
),
),
const SizedBox(height: 12),
DropdownButtonFormField<RouteStopType>(
initialValue: selectedType,
isExpanded: true,
dropdownColor: const Color(0xFF1B1E26),
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
labelText: 'Stop Type',
labelStyle: TextStyle(color: Colors.white60),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Color(0xFF2A2E3D)),
),
),
items: RouteStopType.values.map((type) {
return DropdownMenuItem(
value: type,
child: Row(
children: [
Icon(_getIconForStopType(type),
color: const Color(0xFF00E5FF), size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
type.label,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}).toList(),
onChanged: (val) {
if (val != null) {
setDialogState(() => selectedType = val);
}
},
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: TextField(
controller: latCtrl,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
style: const TextStyle(color: Colors.white, fontSize: 13),
decoration: const InputDecoration(
labelText: 'Latitude *',
labelStyle: TextStyle(color: Colors.white60),
hintText: '37.421998',
hintStyle: TextStyle(color: Colors.white24),
),
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: lngCtrl,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
style: const TextStyle(color: Colors.white, fontSize: 13),
decoration: const InputDecoration(
labelText: 'Longitude *',
labelStyle: TextStyle(color: Colors.white60),
hintText: '-122.084000',
hintStyle: TextStyle(color: Colors.white24),
),
),
),
],
),
const SizedBox(height: 12),
TextField(
controller: notesCtrl,
style: const TextStyle(color: Colors.white),
maxLines: 2,
decoration: const InputDecoration(
labelText: 'Notes (Optional)',
labelStyle: TextStyle(color: Colors.white60),
hintText: 'Regroup point, scenic view, cautions...',
hintStyle: TextStyle(color: Colors.white24),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogCtx),
child: const Text('CANCEL', style: TextStyle(color: Colors.white54)),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black,
),
onPressed: () {
final name = nameCtrl.text.trim();
final lat = double.tryParse(latCtrl.text.trim());
final lng = double.tryParse(lngCtrl.text.trim());
if (name.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please provide a stop name.')),
);
return;
}
if (lat == null || lat < -90.0 || lat > 90.0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invalid latitude (-90 to 90).')),
);
return;
}
if (lng == null || lng < -180.0 || lng > 180.0) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invalid longitude (-180 to 180).')),
);
return;
}
final notifier = ref.read(routePlannerNotifierProvider.notifier);
if (existingStop != null) {
notifier.updateStop(
existingStop.copyWith(
displayName: name,
latitude: lat,
longitude: lng,
stopType: selectedType,
notes: notesCtrl.text.trim(),
),
);
} else {
notifier.addStop(
displayName: name,
latitude: lat,
longitude: lng,
stopType: selectedType,
notes: notesCtrl.text.trim(),
);
}
Navigator.pop(dialogCtx);
},
child: Text(existingStop != null ? 'SAVE' : 'ADD'),
),
],
);
},
),
);
}
Future<void> _saveRoute() async {
final title = _titleController.text.trim();
if (title.length < 3 || title.length > 80) {
setState(() {
_errorMessage = 'Route title must be between 3 and 80 characters.';
});
return;
}
final user = ref.read(currentUserProvider);
if (user == null) {
setState(() {
_errorMessage = 'You must be signed in to save a route.';
});
return;
}
final isPro = ref.read(isProSubscriberProvider);
final state = ref.read(routePlannerNotifierProvider);
final maxAllowed = isPro ? RouteService.maxTechnicalStops : RouteService.freeStopCap;
if (state.stops.length > maxAllowed) {
setState(() {
_errorMessage = isPro
? 'Cannot exceed $maxAllowed stops.'
: 'Free tier limit is $maxAllowed stops. Upgrade to Pro for more.';
});
return;
}
setState(() {
_isSaving = true;
_errorMessage = null;
});
try {
final notifier = ref.read(routePlannerNotifierProvider.notifier);
final savedRoute = await notifier.saveRoute(
title: title,
description: _descController.text.trim(),
ownerUid: user.uid,
visibility: _visibility,
attachedGroupRideId: widget.attachToGroupRideId,
);
// If attached to a group ride, update the group ride with route reference
if (widget.attachToGroupRideId != null) {
final service = ref.read(routeServiceProvider);
await service.attachRouteToGroupRide(
groupRideId: widget.attachToGroupRideId!,
route: savedRoute,
);
}
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Route "${savedRoute.title}" saved successfully!'),
backgroundColor: const Color(0xFF00E676),
),
);
context.pop(savedRoute);
}
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = 'Failed to save route: $e';
_isSaving = false;
});
}
}
}
IconData _getIconForStopType(RouteStopType type) {
switch (type) {
case RouteStopType.start:
case RouteStopType.origin:
return Icons.play_circle_fill;
case RouteStopType.meetup:
return Icons.people;
case RouteStopType.waypoint:
return Icons.place;
case RouteStopType.fuel:
return Icons.local_gas_station;
case RouteStopType.rest:
return Icons.hotel;
case RouteStopType.food:
return Icons.restaurant;
case RouteStopType.scenic:
return Icons.photo_camera;
case RouteStopType.regroup:
return Icons.group;
case RouteStopType.destination:
return Icons.flag;
case RouteStopType.other:
return Icons.location_on;
}
}
@override
Widget build(BuildContext context) {
if (!_initialized) {
return const Scaffold(
backgroundColor: Color(0xFF0D0E11),
body: Center(child: CircularProgressIndicator(color: Color(0xFF00E5FF))),
);
}
final state = ref.watch(routePlannerNotifierProvider);
final isPro = ref.watch(isProSubscriberProvider);
final maxAllowed = isPro ? RouteService.maxTechnicalStops : RouteService.freeStopCap;
final isAtLimit = state.stops.length >= maxAllowed;
// Center map on first stop or default
final mapCenter = state.stops.isNotEmpty
? AppLatLng(state.stops.first.latitude, state.stops.first.longitude)
: const AppLatLng(37.7749, -122.4194);
// Build markers
final markers = state.stops.map((stop) {
final isFirst = stop.orderIndex == 0;
final isLast = stop.orderIndex == state.stops.length - 1 && state.stops.length > 1;
return AppMapMarker(
id: stop.id,
position: AppLatLng(stop.latitude, stop.longitude),
label: stop.displayName,
iconWidget: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: isFirst
? const Color(0xFF00E676)
: isLast
? const Color(0xFFFF5252)
: const Color(0xFF00E5FF),
shape: BoxShape.circle,
boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 4, spreadRadius: 1),
],
),
child: Text(
'${stop.orderIndex + 1}',
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
onTap: () {
NavigationHandoffService.showHandoffSheet(
context: context,
latitude: stop.latitude,
longitude: stop.longitude,
displayName: stop.displayName,
);
},
);
}).toList();
// Build polylines
final polylines = state.stops.length >= 2
? [
AppPolyline(
id: 'straight_line_preview',
points: state.previewPoints
.map((p) => AppLatLng(p.latitude, p.longitude))
.toList(),
color: const Color(0xFF00E5FF),
width: 3.5,
),
]
: <AppPolyline>[];
return Scaffold(
backgroundColor: const Color(0xFF0D0E11),
appBar: AppBar(
backgroundColor: const Color(0xFF13151A),
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => context.pop(),
),
title: Text(
widget.initialRouteId != null ? 'EDIT ROUTE' : 'ROUTE PLANNER',
style: const TextStyle(
fontWeight: FontWeight.w900,
letterSpacing: 1.2,
fontSize: 16,
),
),
actions: [
if (_isSaving)
const Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFF00E5FF),
),
),
),
)
else
TextButton.icon(
onPressed: _saveRoute,
icon: const Icon(Icons.save, color: Color(0xFF00E5FF), size: 18),
label: const Text(
'SAVE',
style: TextStyle(
color: Color(0xFF00E5FF),
fontWeight: FontWeight.bold,
),
),
),
],
),
body: LayoutBuilder(
builder: (context, constraints) {
final isNarrow = constraints.maxWidth < 360;
return Column(
children: [
// Error banner if any
if (_errorMessage != null)
Container(
width: double.infinity,
color: const Color(0xFFFF5252).withValues(alpha: 0.15),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
const Icon(Icons.error_outline,
color: Color(0xFFFF5252), size: 18),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: const TextStyle(
color: Color(0xFFFF5252),
fontSize: 12,
),
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white54, size: 16),
onPressed: () => setState(() => _errorMessage = null),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
),
// Map Preview Section (220dp high)
SizedBox(
height: 220,
width: double.infinity,
child: Stack(
children: [
UnifiedMapView(
center: mapCenter,
zoom: 12.0,
markers: markers,
polylines: polylines,
),
// Distance & Stops HUD overlay
Positioned(
top: 8,
left: 8,
right: 8,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF13151A).withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFF2A2E3D)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
state.stops.length >= 2
? 'Straight-line: ${state.formattedDistanceMiles} (${state.formattedDistanceKm})'
: 'Add 2+ stops for distance',
style: const TextStyle(
color: Color(0xFF00E5FF),
fontSize: 12,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: isAtLimit
? const Color(0xFFFFB300).withValues(alpha: 0.2)
: const Color(0xFF00E5FF).withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'${state.stops.length}/$maxAllowed STOPS',
style: TextStyle(
color: isAtLimit
? const Color(0xFFFFB300)
: const Color(0xFF00E5FF),
fontSize: 10,
fontWeight: FontWeight.w900,
),
),
),
],
),
),
),
// Straight-line disclaimer note
Positioned(
bottom: 4,
left: 8,
right: 8,
child: Text(
'Straight-line preview. Navigation handoff launches in Google Maps / Waze.',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.65),
fontSize: 9,
shadows: const [
Shadow(color: Colors.black, blurRadius: 4),
],
),
),
),
],
),
),
// Route Info Inputs (collapsible / compact)
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Column(
children: [
TextField(
controller: _titleController,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15,
),
decoration: const InputDecoration(
hintText: 'Route Title (e.g. Skyline Ridge Loop) *',
hintStyle: TextStyle(color: Colors.white30, fontSize: 14),
isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 8),
border: InputBorder.none,
),
),
Wrap(
spacing: 8,
runSpacing: 4,
alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
PopupMenuButton<RouteVisibility>(
initialValue: _visibility,
onSelected: (v) => setState(() => _visibility = v),
color: const Color(0xFF1B1E26),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.visibility_outlined,
size: 14, color: Colors.white54),
const SizedBox(width: 4),
Text(
_visibility.label,
style: const TextStyle(
color: Color(0xFF00E5FF),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const Icon(Icons.arrow_drop_down,
size: 16, color: Color(0xFF00E5FF)),
],
),
),
itemBuilder: (context) => RouteVisibility.values.map((v) {
return PopupMenuItem(
value: v,
child: Text(
v.label,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
),
),
);
}).toList(),
),
if (!isPro && isAtLimit)
TextButton(
onPressed: () => context.push('/settings/pro'),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: const Text(
'Upgrade for 25 Stops',
style: TextStyle(
color: Color(0xFFFFB300),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
const Divider(color: Color(0xFF2A2E3D), height: 1),
// Stop List Controls (Add Stop / Add GPS / Reverse)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.spaceBetween,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton.icon(
onPressed: () => _showAddOrEditStopDialog(),
icon: const Icon(Icons.add_location_alt, size: 16),
label: Text(isNarrow ? 'ADD' : 'ADD STOP'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 8),
textStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: _addCurrentLocationStop,
icon: const Icon(Icons.my_location,
size: 16, color: Color(0xFF00E5FF)),
label: Text(isNarrow ? 'GPS' : 'CURRENT GPS',
style: const TextStyle(color: Color(0xFF00E5FF))),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFF2A2E3D)),
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 8),
textStyle: const TextStyle(fontSize: 12),
),
),
],
),
if (state.stops.length >= 2)
TextButton.icon(
onPressed: () =>
ref.read(routePlannerNotifierProvider.notifier).reverseStops(),
icon: const Icon(Icons.swap_vert,
size: 16, color: Colors.white70),
label: const Text('REVERSE',
style: TextStyle(color: Colors.white70, fontSize: 11)),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 4),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
],
),
),
// Reorderable Stop List
Expanded(
child: state.stops.isEmpty
? Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.alt_route,
size: 48,
color: Colors.white.withValues(alpha: 0.15)),
const SizedBox(height: 12),
const Text(
'No stops added yet.',
style: TextStyle(
color: Colors.white54, fontSize: 14),
),
const SizedBox(height: 6),
const Text(
'Tap "Add Stop" or "Current GPS" to begin planning.',
style: TextStyle(
color: Colors.white30, fontSize: 12),
),
],
),
)
: ReorderableListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 4),
itemCount: state.stops.length,
// ignore: deprecated_member_use
onReorder: (oldIdx, newIdx) {
ref
.read(routePlannerNotifierProvider.notifier)
.reorderStops(oldIdx, newIdx);
},
itemBuilder: (context, index) {
final stop = state.stops[index];
final isFirst = index == 0;
final isLast =
index == state.stops.length - 1 && state.stops.length > 1;
return Container(
key: ValueKey(stop.id),
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: const Color(0xFF13151A),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF2A2E3D)),
),
child: Row(
children: [
// Order badge
Container(
width: 26,
height: 26,
decoration: BoxDecoration(
color: isFirst
? const Color(0xFF00E676)
: isLast
? const Color(0xFFFF5252)
: const Color(0xFF00E5FF),
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Text(
'${index + 1}',
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
const SizedBox(width: 10),
// Stop Details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
_getIconForStopType(stop.stopType),
size: 14,
color: const Color(0xFF00E5FF),
),
const SizedBox(width: 4),
Flexible(
child: Text(
stop.displayName,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 2),
Text(
'${stop.latitude.toStringAsFixed(4)}, ${stop.longitude.toStringAsFixed(4)}',
style: const TextStyle(
color: Colors.white38,
fontSize: 11,
),
),
if (stop.notes.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
stop.notes,
style: const TextStyle(
color: Colors.white70,
fontSize: 11,
fontStyle: FontStyle.italic,
),
overflow: TextOverflow.ellipsis,
),
],
],
),
),
// Actions: Handoff navigation, Edit, Delete
IconButton(
icon: const Icon(Icons.navigation,
color: Color(0xFF00E5FF), size: 18),
tooltip: 'Open in Maps',
onPressed: () {
NavigationHandoffService.showHandoffSheet(
context: context,
latitude: stop.latitude,
longitude: stop.longitude,
displayName: stop.displayName,
);
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32),
),
IconButton(
icon: const Icon(Icons.edit,
color: Colors.white60, size: 18),
tooltip: 'Edit stop',
onPressed: () => _showAddOrEditStopDialog(
existingStop: stop,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32),
),
IconButton(
icon: const Icon(Icons.delete_outline,
color: Color(0xFFFF5252), size: 18),
tooltip: 'Remove stop',
onPressed: () {
ref
.read(routePlannerNotifierProvider.notifier)
.removeStop(stop.id);
},
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32),
),
const SizedBox(width: 4),
const Icon(Icons.drag_handle,
color: Colors.white30, size: 20),
],
),
);
},
),
),
],
);
},
),
);
}
}