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

662 lines
27 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../services/auth/auth_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';
class RouteDetailScreen extends ConsumerStatefulWidget {
final String routeId;
const RouteDetailScreen({super.key, required this.routeId});
@override
ConsumerState<RouteDetailScreen> createState() => _RouteDetailScreenState();
}
class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
bool _isDeleting = false;
Future<void> _confirmDelete(MotorSignalRoute route) async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF13151A),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Color(0xFF2A2E3D)),
),
title: const Text(
'Delete Route',
style: TextStyle(color: Colors.white),
),
content: Text(
'Are you sure you want to delete "${route.title}"? This cannot be undone.',
style: const TextStyle(color: Colors.white70),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text(
'CANCEL',
style: TextStyle(color: Colors.white54),
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFF5252),
foregroundColor: Colors.white,
),
onPressed: () => Navigator.pop(ctx, true),
child: const Text('DELETE'),
),
],
),
);
if (confirm != true || !mounted) return;
setState(() => _isDeleting = true);
try {
final user = ref.read(currentUserProvider);
if (user == null || user.uid.isEmpty) {
throw StateError('Sign-in required to delete route.');
}
final service = ref.read(routeServiceProvider);
await service.deleteRoute(
widget.routeId,
callerUid: user.uid,
);
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Route deleted.')));
context.pop();
}
} catch (e) {
if (mounted) {
setState(() => _isDeleting = false);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Failed to delete route: $e')));
}
}
}
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) {
final routeAsync = ref.watch(routeByIdProvider(widget.routeId));
final currentUser = ref.watch(currentUserProvider);
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: const Text(
'ROUTE DETAILS',
style: TextStyle(
fontWeight: FontWeight.w900,
letterSpacing: 1.2,
fontSize: 16,
),
),
),
body: routeAsync.when(
loading: () => const Center(
child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
),
error: (err, _) => Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error_outline,
size: 48,
color: Color(0xFFFF5252),
),
const SizedBox(height: 12),
Text(
'Could not load route: $err',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white70),
),
],
),
),
),
data: (route) {
if (route == null) {
return const Center(
child: Text(
'Route not found or has been deleted.',
style: TextStyle(color: Colors.white54),
),
);
}
final isOwner = currentUser?.uid == route.ownerUid;
// Map markers
final markers = route.stops.map((stop) {
final isFirst = stop.orderIndex == 0;
final isLast =
stop.orderIndex == route.stops.length - 1 &&
route.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();
// Polylines
final polylines = route.stops.length >= 2
? [
AppPolyline(
id: 'route_straight_line',
points: route.previewPoints
.map((p) => AppLatLng(p.latitude, p.longitude))
.toList(),
color: const Color(0xFF00E5FF),
width: 3.5,
),
]
: <AppPolyline>[];
final mapCenter = route.stops.isNotEmpty
? AppLatLng(
route.stops.first.latitude,
route.stops.first.longitude,
)
: const AppLatLng(37.7749, -122.4194);
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Map Preview
SizedBox(
height: 240,
width: double.infinity,
child: Stack(
children: [
UnifiedMapView(
center: mapCenter,
zoom: 12.0,
markers: markers,
polylines: polylines,
),
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(
0xFF13151A,
).withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFF2A2E3D)),
),
child: Text(
route.visibility.label,
style: const TextStyle(
color: Color(0xFF00E5FF),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
// Title & Info
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
route.title,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
if (route.description.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
route.description,
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
],
const SizedBox(height: 12),
// Distance and Stops Badges
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF13151A),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF2A2E3D)),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'STRAIGHT-LINE DISTANCE',
style: TextStyle(
color: Colors.white38,
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 1.0,
),
),
const SizedBox(height: 2),
Text(
'${route.formattedDistanceMiles} (${route.formattedDistanceKm})',
style: const TextStyle(
color: Color(0xFF00E5FF),
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
],
),
),
Container(
width: 1,
height: 32,
color: const Color(0xFF2A2E3D),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'WAYPOINTS',
style: TextStyle(
color: Colors.white38,
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 1.0,
),
),
const SizedBox(height: 2),
Text(
'${route.stops.length} STOPS',
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
),
const SizedBox(height: 8),
// Honest Disclaimer
const Text(
'MotorSignal provides straight-line route planning and handoff. Turn-by-turn navigation opens in Google Maps or Waze.',
style: TextStyle(color: Colors.white38, fontSize: 11),
),
const SizedBox(height: 16),
// Owner Actions: Edit & Delete
if (isOwner) ...[
Row(
children: [
Expanded(
child: OutlinedButton.icon(
key: const ValueKey('route_detail_edit_button'),
onPressed: () {
context.push(
'/routes/planner?routeId=${route.id}',
);
},
icon: const Icon(
Icons.edit,
size: 16,
color: Color(0xFF00E5FF),
),
label: const Text(
'EDIT ROUTE',
style: TextStyle(color: Color(0xFF00E5FF)),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Color(0xFF00E5FF),
),
padding: const EdgeInsets.symmetric(
vertical: 10,
),
),
),
),
const SizedBox(width: 12),
OutlinedButton.icon(
key: const ValueKey('route_detail_delete_button'),
onPressed: _isDeleting
? null
: () => _confirmDelete(route),
icon: const Icon(
Icons.delete_outline,
size: 16,
color: Color(0xFFFF5252),
),
label: const Text(
'DELETE',
style: TextStyle(color: Color(0xFFFF5252)),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(
color: Color(0xFFFF5252),
),
padding: const EdgeInsets.symmetric(
vertical: 10,
horizontal: 16,
),
),
),
],
),
const SizedBox(height: 16),
],
// Attached Group Ride button if any
if (route.attachedGroupRideId != null) ...[
OutlinedButton.icon(
key: const ValueKey(
'route_detail_view_attached_group_ride_button',
),
onPressed: () {
context.push(
'/group-rides/${route.attachedGroupRideId}',
);
},
icon: const Icon(
Icons.group,
size: 18,
color: Color(0xFF00E5FF),
),
label: const Text(
'VIEW ATTACHED GROUP RIDE',
style: TextStyle(color: Color(0xFF00E5FF)),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFF2A2E3D)),
minimumSize: const Size(double.infinity, 44),
),
),
const SizedBox(height: 16),
],
// Stops List Header
const Text(
'STOPS & WAYPOINTS',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
),
),
const SizedBox(height: 10),
// Stops List
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: route.stops.length,
itemBuilder: (context, index) {
final stop = route.stops[index];
final isFirst = index == 0;
final isLast =
index == route.stops.length - 1 &&
route.stops.length > 1;
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF13151A),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: const Color(0xFF2A2E3D),
),
),
child: Row(
children: [
Container(
width: 28,
height: 28,
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: 13,
),
),
),
const SizedBox(width: 12),
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,
),
),
),
],
),
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: 12,
fontStyle: FontStyle.italic,
),
),
],
],
),
),
ElevatedButton.icon(
onPressed: () {
NavigationHandoffService.showHandoffSheet(
context: context,
latitude: stop.latitude,
longitude: stop.longitude,
displayName: stop.displayName,
);
},
icon: const Icon(Icons.navigation, size: 14),
label: const Text('NAV'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
textStyle: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
},
),
const SizedBox(height: 24),
// Primary action: Navigate to First Stop
if (route.stops.isNotEmpty)
ElevatedButton.icon(
key: const ValueKey(
'route_detail_navigate_start_button',
),
onPressed: () {
final firstStop = route.stops.first;
NavigationHandoffService.showHandoffSheet(
context: context,
latitude: firstStop.latitude,
longitude: firstStop.longitude,
displayName: 'Start: ${firstStop.displayName}',
);
},
icon: const Icon(Icons.directions, size: 20),
label: const Text(
'NAVIGATE TO ROUTE START',
style: TextStyle(
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black,
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(height: 32),
],
),
),
],
),
);
},
),
);
}
}