chore: add debug-only Marionette test bridge

This commit is contained in:
Developer 2026-09-04 12:45:01 -04:00
parent 23e63350b0
commit cbeccb6063
14 changed files with 1214 additions and 460 deletions

View File

@ -33,13 +33,15 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ref.listen<RideSession>(rideStateMachineProvider, (previous, next) { ref.listen<RideSession>(rideStateMachineProvider, (previous, next) {
if (previous?.state != RideState.riding && next.state == RideState.riding) { if (previous?.state != RideState.riding &&
next.state == RideState.riding) {
if (widget.navigationShell.currentIndex != 0) { if (widget.navigationShell.currentIndex != 0) {
widget.navigationShell.goBranch(0); widget.navigationShell.goBranch(0);
} }
} }
}); });
final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape; final isLandscape =
MediaQuery.of(context).orientation == Orientation.landscape;
if (isLandscape) { if (isLandscape) {
return Scaffold( return Scaffold(
@ -52,7 +54,9 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
return SingleChildScrollView( return SingleChildScrollView(
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight), constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight( child: IntrinsicHeight(
child: NavigationRail( child: NavigationRail(
minWidth: 54, minWidth: 54,
@ -61,13 +65,22 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
onDestinationSelected: (index) { onDestinationSelected: (index) {
widget.navigationShell.goBranch( widget.navigationShell.goBranch(
index, index,
initialLocation: index == widget.navigationShell.currentIndex, initialLocation:
index == widget.navigationShell.currentIndex,
); );
}, },
backgroundColor: const Color(0xFF0A0A0A), backgroundColor: const Color(0xFF0A0A0A),
indicatorColor: const Color(0xFF00E5FF).withValues(alpha: 0.18), indicatorColor: const Color(
selectedIconTheme: const IconThemeData(color: Color(0xFF00E5FF), size: 20), 0xFF00E5FF,
unselectedIconTheme: const IconThemeData(color: Colors.white54, size: 18), ).withValues(alpha: 0.18),
selectedIconTheme: const IconThemeData(
color: Color(0xFF00E5FF),
size: 20,
),
unselectedIconTheme: const IconThemeData(
color: Colors.white54,
size: 18,
),
selectedLabelTextStyle: const TextStyle( selectedLabelTextStyle: const TextStyle(
fontFamily: 'Inter', fontFamily: 'Inter',
color: Color(0xFF00E5FF), color: Color(0xFF00E5FF),
@ -84,32 +97,50 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
destinations: const [ destinations: const [
NavigationRailDestination( NavigationRailDestination(
padding: EdgeInsets.symmetric(vertical: 2), padding: EdgeInsets.symmetric(vertical: 2),
icon: Icon(Icons.speed), icon: Icon(
Icons.speed,
key: ValueKey('rail_dash_icon'),
),
label: Text('Dash'), label: Text('Dash'),
), ),
NavigationRailDestination( NavigationRailDestination(
padding: EdgeInsets.symmetric(vertical: 2), padding: EdgeInsets.symmetric(vertical: 2),
icon: Icon(Icons.map_outlined), icon: Icon(
Icons.map_outlined,
key: ValueKey('rail_map_icon'),
),
label: Text('Map'), label: Text('Map'),
), ),
NavigationRailDestination( NavigationRailDestination(
padding: EdgeInsets.symmetric(vertical: 2), padding: EdgeInsets.symmetric(vertical: 2),
icon: Icon(Icons.history), icon: Icon(
Icons.history,
key: ValueKey('rail_history_icon'),
),
label: Text('History'), label: Text('History'),
), ),
NavigationRailDestination( NavigationRailDestination(
padding: EdgeInsets.symmetric(vertical: 2), padding: EdgeInsets.symmetric(vertical: 2),
icon: Icon(Icons.emoji_events_outlined), icon: Icon(
Icons.emoji_events_outlined,
key: ValueKey('rail_records_icon'),
),
label: Text('Records'), label: Text('Records'),
), ),
NavigationRailDestination( NavigationRailDestination(
padding: EdgeInsets.symmetric(vertical: 2), padding: EdgeInsets.symmetric(vertical: 2),
icon: Icon(Icons.two_wheeler), icon: Icon(
Icons.two_wheeler,
key: ValueKey('rail_garage_icon'),
),
label: Text('Garage'), label: Text('Garage'),
), ),
NavigationRailDestination( NavigationRailDestination(
padding: EdgeInsets.symmetric(vertical: 2), padding: EdgeInsets.symmetric(vertical: 2),
icon: Icon(Icons.settings_outlined), icon: Icon(
Icons.settings_outlined,
key: ValueKey('rail_settings_icon'),
),
label: Text('Settings'), label: Text('Settings'),
), ),
], ],
@ -120,7 +151,11 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
}, },
), ),
), ),
const VerticalDivider(width: 1, thickness: 1, color: Color(0xFF1F1F1F)), const VerticalDivider(
width: 1,
thickness: 1,
color: Color(0xFF1F1F1F),
),
Expanded(child: widget.navigationShell), Expanded(child: widget.navigationShell),
], ],
), ),
@ -130,6 +165,7 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
return Scaffold( return Scaffold(
body: widget.navigationShell, body: widget.navigationShell,
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
key: const ValueKey('main_bottom_nav_bar'),
currentIndex: widget.navigationShell.currentIndex, currentIndex: widget.navigationShell.currentIndex,
onTap: (index) { onTap: (index) {
widget.navigationShell.goBranch( widget.navigationShell.goBranch(
@ -143,28 +179,34 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
items: const [ items: const [
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.speed), icon: Icon(Icons.speed, key: ValueKey('nav_dash_icon')),
label: 'Dash', label: 'Dash',
tooltip: 'Dash',
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.map), icon: Icon(Icons.map, key: ValueKey('nav_map_icon')),
label: 'Map', label: 'Map',
tooltip: 'Map',
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.history), icon: Icon(Icons.history, key: ValueKey('nav_history_icon')),
label: 'History', label: 'History',
tooltip: 'History',
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.emoji_events), icon: Icon(Icons.emoji_events, key: ValueKey('nav_records_icon')),
label: 'Records', label: 'Records',
tooltip: 'Records',
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.two_wheeler), icon: Icon(Icons.two_wheeler, key: ValueKey('nav_garage_icon')),
label: 'Garage', label: 'Garage',
tooltip: 'Garage',
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.settings), icon: Icon(Icons.settings, key: ValueKey('nav_settings_icon')),
label: 'Settings', label: 'Settings',
tooltip: 'Settings',
), ),
], ],
), ),

View File

@ -39,6 +39,7 @@ class GarageScreen extends ConsumerWidget {
), ),
actions: [ actions: [
IconButton( IconButton(
key: const ValueKey('garage_add_motorcycle_appbar_button'),
icon: const Icon( icon: const Icon(
Icons.add_circle_outline, Icons.add_circle_outline,
color: Color(0xFF00E5FF), color: Color(0xFF00E5FF),
@ -71,6 +72,7 @@ class GarageScreen extends ConsumerWidget {
floatingActionButton: floatingActionButton:
bikesAsync.value != null && bikesAsync.value!.isNotEmpty bikesAsync.value != null && bikesAsync.value!.isNotEmpty
? FloatingActionButton.extended( ? FloatingActionButton.extended(
key: const ValueKey('garage_add_motorcycle_fab'),
backgroundColor: Colors.cyanAccent, backgroundColor: Colors.cyanAccent,
foregroundColor: Colors.black, foregroundColor: Colors.black,
icon: const Icon(Icons.two_wheeler), icon: const Icon(Icons.two_wheeler),
@ -130,6 +132,7 @@ class GarageScreen extends ConsumerWidget {
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
ElevatedButton.icon( ElevatedButton.icon(
key: const ValueKey('garage_add_motorcycle_empty_button'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.cyanAccent, backgroundColor: Colors.cyanAccent,
foregroundColor: Colors.black, foregroundColor: Colors.black,

File diff suppressed because it is too large Load Diff

View File

@ -72,6 +72,7 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
actions: [ actions: [
if (isAuthed) if (isAuthed)
IconButton( IconButton(
key: const ValueKey('group_rides_create_appbar_button'),
icon: const Icon(Icons.add, color: Color(0xFF00E5FF)), icon: const Icon(Icons.add, color: Color(0xFF00E5FF)),
tooltip: 'Create Group Ride', tooltip: 'Create Group Ride',
onPressed: () => context.push('/group-rides/create'), onPressed: () => context.push('/group-rides/create'),
@ -89,6 +90,7 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
), ),
floatingActionButton: isAuthed floatingActionButton: isAuthed
? FloatingActionButton.extended( ? FloatingActionButton.extended(
key: const ValueKey('create_group_ride_fab'),
backgroundColor: const Color(0xFF00E5FF), backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black, foregroundColor: Colors.black,
icon: const Icon(Icons.add_road), icon: const Icon(Icons.add_road),
@ -134,15 +136,21 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
const Text( const Text(
'To organize group rides, invite fellow riders, and coordinate meeting points, sign in with your permanent MotorSignal account.', 'To organize group rides, invite fellow riders, and coordinate meeting points, sign in with your permanent MotorSignal account.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: Colors.white70, fontSize: 13, height: 1.4), style: TextStyle(
color: Colors.white70,
fontSize: 13,
height: 1.4,
),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF), backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black, foregroundColor: Colors.black,
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 24, vertical: 14), horizontal: 24,
vertical: 14,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
@ -161,7 +169,10 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
} }
Widget _buildUpcomingTab( Widget _buildUpcomingTab(
BuildContext context, WidgetRef ref, String currentUid) { BuildContext context,
WidgetRef ref,
String currentUid,
) {
final ridesAsync = ref.watch(upcomingGroupRidesProvider); final ridesAsync = ref.watch(upcomingGroupRidesProvider);
return ridesAsync.when( return ridesAsync.when(
@ -190,20 +201,26 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
child: CircularProgressIndicator(color: Color(0xFF00E5FF)), child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
), ),
error: (e, _) => Center( error: (e, _) => Center(
child: Text('Error loading rides: $e', child: Text(
style: const TextStyle(color: Colors.redAccent)), 'Error loading rides: $e',
style: const TextStyle(color: Colors.redAccent),
),
), ),
); );
} }
Widget _buildMyRidesTab( Widget _buildMyRidesTab(
BuildContext context, WidgetRef ref, String currentUid) { BuildContext context,
WidgetRef ref,
String currentUid,
) {
final ridesAsync = ref.watch(upcomingGroupRidesProvider); final ridesAsync = ref.watch(upcomingGroupRidesProvider);
return ridesAsync.when( return ridesAsync.when(
data: (rides) { data: (rides) {
final myRides = final myRides = rides
rides.where((r) => r.organizerUid == currentUid).toList(); .where((r) => r.organizerUid == currentUid)
.toList();
if (myRides.isEmpty) { if (myRides.isEmpty) {
return _buildEmptyState( return _buildEmptyState(
@ -229,8 +246,10 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
child: CircularProgressIndicator(color: Color(0xFF00E5FF)), child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
), ),
error: (e, _) => Center( error: (e, _) => Center(
child: Text('Error: $e', child: Text(
style: const TextStyle(color: Colors.redAccent)), 'Error: $e',
style: const TextStyle(color: Colors.redAccent),
),
), ),
); );
} }
@ -283,7 +302,10 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
} }
Widget _buildRideCard( Widget _buildRideCard(
BuildContext context, GroupRide ride, String currentUid) { BuildContext context,
GroupRide ride,
String currentUid,
) {
final isOrganizer = ride.organizerUid == currentUid; final isOrganizer = ride.organizerUid == currentUid;
return Card( return Card(
@ -298,6 +320,7 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
), ),
), ),
child: InkWell( child: InkWell(
key: ValueKey('group_ride_card_${ride.id}'),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
onTap: () => context.push('/group-rides/${ride.id}'), onTap: () => context.push('/group-rides/${ride.id}'),
child: Padding( child: Padding(
@ -308,8 +331,10 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
Row( Row(
children: [ children: [
Container( Container(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 8, vertical: 4), horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: ride.isCanceled color: ride.isCanceled
? Colors.redAccent.withValues(alpha: 0.2) ? Colors.redAccent.withValues(alpha: 0.2)
@ -331,7 +356,9 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
if (isOrganizer) if (isOrganizer)
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2), horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.amber.withValues(alpha: 0.2), color: Colors.amber.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
@ -348,8 +375,11 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
const Spacer(), const Spacer(),
Row( Row(
children: [ children: [
const Icon(Icons.people_outline, const Icon(
size: 16, color: Colors.white60), Icons.people_outline,
size: 16,
color: Colors.white60,
),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'${ride.participantCount}${ride.participantLimit != null ? '/${ride.participantLimit}' : ''}', '${ride.participantCount}${ride.participantLimit != null ? '/${ride.participantLimit}' : ''}',
@ -375,7 +405,11 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
const SizedBox(height: 6), const SizedBox(height: 6),
Row( Row(
children: [ children: [
const Icon(Icons.schedule, size: 14, color: Color(0xFF00E5FF)), const Icon(
Icons.schedule,
size: 14,
color: Color(0xFF00E5FF),
),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
'Meet: ${_formatDateTime(ride.scheduledStartAt)} • KSU: ${_formatTime(ride.departureAt)}', 'Meet: ${_formatDateTime(ride.scheduledStartAt)} • KSU: ${_formatTime(ride.departureAt)}',
@ -390,8 +424,11 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
const SizedBox(height: 4), const SizedBox(height: 4),
Row( Row(
children: [ children: [
const Icon(Icons.place_outlined, const Icon(
size: 14, color: Colors.white54), Icons.place_outlined,
size: 14,
color: Colors.white54,
),
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(
child: Text( child: Text(
@ -409,8 +446,11 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
const SizedBox(height: 4), const SizedBox(height: 4),
Row( Row(
children: [ children: [
const Icon(Icons.flag_outlined, const Icon(
size: 14, color: Color(0xFFFFB300)), Icons.flag_outlined,
size: 14,
color: Color(0xFFFFB300),
),
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(
child: Text( child: Text(

View File

@ -49,14 +49,18 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final rideAsync = ref.watch(groupRideStreamProvider(widget.rideId)); final rideAsync = ref.watch(groupRideStreamProvider(widget.rideId));
final participantsAsync = final participantsAsync = ref.watch(
ref.watch(groupRideParticipantsProvider(widget.rideId)); groupRideParticipantsProvider(widget.rideId),
final presencesAsync = );
ref.watch(groupRideLivePresenceProvider(widget.rideId)); final presencesAsync = ref.watch(
final broadcastState = groupRideLivePresenceProvider(widget.rideId),
ref.watch(liveBroadcastControllerProvider(widget.rideId)); );
final broadcastController = final broadcastState = ref.watch(
ref.read(liveBroadcastControllerProvider(widget.rideId).notifier); liveBroadcastControllerProvider(widget.rideId),
);
final broadcastController = ref.read(
liveBroadcastControllerProvider(widget.rideId).notifier,
);
final user = ref.watch(currentUserProvider); final user = ref.watch(currentUserProvider);
final currentUid = user?.uid; final currentUid = user?.uid;
@ -78,7 +82,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
); );
} }
final isOrganizer = currentUid != null && ride.organizerUid == currentUid; final isOrganizer =
currentUid != null && ride.organizerUid == currentUid;
// Check if ride was closed // Check if ride was closed
if (ride.isCompleted || ride.isCanceled) { if (ride.isCompleted || ride.isCanceled) {
@ -103,7 +108,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
children: [ children: [
Icon( Icon(
ride.isCompleted ? Icons.flag : Icons.cancel, ride.isCompleted ? Icons.flag : Icons.cancel,
color: ride.isCompleted ? Colors.greenAccent : Colors.redAccent, color: ride.isCompleted
? Colors.greenAccent
: Colors.redAccent,
size: 48, size: 48,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@ -136,28 +143,32 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
return participantsAsync.when( return participantsAsync.when(
data: (participants) { data: (participants) {
final acceptedParticipants = final acceptedParticipants = participants
participants.where((p) => p.status == ParticipantStatus.accepted).toList(); .where((p) => p.status == ParticipantStatus.accepted)
.toList();
return presencesAsync.when( return presencesAsync.when(
data: (presences) { data: (presences) {
// Determine Ride Lead (first participant with lead role, or organizer) // Determine Ride Lead (first participant with lead role, or organizer)
GroupRideParticipant? leadParticipant; GroupRideParticipant? leadParticipant;
try { try {
leadParticipant = acceptedParticipants leadParticipant = acceptedParticipants.firstWhere(
.firstWhere((p) => p.role == GroupRideRole.lead); (p) => p.role == GroupRideRole.lead,
);
} catch (_) { } catch (_) {
try { try {
leadParticipant = acceptedParticipants leadParticipant = acceptedParticipants.firstWhere(
.firstWhere((p) => p.role == GroupRideRole.organizer); (p) => p.role == GroupRideRole.organizer,
);
} catch (_) {} } catch (_) {}
} }
LiveGroupPresence? leadPresence; LiveGroupPresence? leadPresence;
if (leadParticipant != null) { if (leadParticipant != null) {
try { try {
leadPresence = presences leadPresence = presences.firstWhere(
.firstWhere((p) => p.userId == leadParticipant!.userId); (p) => p.userId == leadParticipant!.userId,
);
} catch (_) {} } catch (_) {}
} }
@ -165,7 +176,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
LiveGroupPresence? regroupCaller; LiveGroupPresence? regroupCaller;
try { try {
regroupCaller = presences.firstWhere( regroupCaller = presences.firstWhere(
(p) => p.status == LivePresenceStatus.regrouping && p.isFresh, (p) =>
p.status == LivePresenceStatus.regrouping && p.isFresh,
); );
} catch (_) {} } catch (_) {}
@ -246,7 +258,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
context: context, context: context,
caller: regroupCaller, caller: regroupCaller,
participants: acceptedParticipants, participants: acceptedParticipants,
isLeadOrOrganizer: isOrganizer || isLeadOrOrganizer:
isOrganizer ||
(leadParticipant != null && (leadParticipant != null &&
leadParticipant.userId == currentUid), leadParticipant.userId == currentUid),
controller: broadcastController, controller: broadcastController,
@ -304,8 +317,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
error: (e, _) => Scaffold( error: (e, _) => Scaffold(
backgroundColor: const Color(0xFF0D0E11), backgroundColor: const Color(0xFF0D0E11),
body: Center( body: Center(
child: Text('Error loading live presence: $e', child: Text(
style: const TextStyle(color: Colors.redAccent)), 'Error loading live presence: $e',
style: const TextStyle(color: Colors.redAccent),
),
), ),
), ),
); );
@ -319,8 +334,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
error: (e, _) => Scaffold( error: (e, _) => Scaffold(
backgroundColor: const Color(0xFF0D0E11), backgroundColor: const Color(0xFF0D0E11),
body: Center( body: Center(
child: Text('Error loading participants: $e', child: Text(
style: const TextStyle(color: Colors.redAccent)), 'Error loading participants: $e',
style: const TextStyle(color: Colors.redAccent),
),
), ),
), ),
); );
@ -334,8 +351,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
error: (e, _) => Scaffold( error: (e, _) => Scaffold(
backgroundColor: const Color(0xFF0D0E11), backgroundColor: const Color(0xFF0D0E11),
body: Center( body: Center(
child: Text('Error loading ride: $e', child: Text(
style: const TextStyle(color: Colors.redAccent)), 'Error loading ride: $e',
style: const TextStyle(color: Colors.redAccent),
),
), ),
), ),
); );
@ -406,7 +425,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
actions: [ actions: [
if (isOrganizer) if (isOrganizer)
IconButton( IconButton(
icon: const Icon(Icons.stop_circle_outlined, color: Colors.redAccent), icon: const Icon(
Icons.stop_circle_outlined,
color: Colors.redAccent,
),
tooltip: 'End Ride', tooltip: 'End Ride',
onPressed: () => _confirmEndRide(context, ride, currentUid), onPressed: () => _confirmEndRide(context, ride, currentUid),
), ),
@ -435,7 +457,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
String callerName = caller.userId; String callerName = caller.userId;
try { try {
final p = participants.firstWhere((p) => p.userId == caller.userId); final p = participants.firstWhere((p) => p.userId == caller.userId);
callerName = p.riderDisplayName.isNotEmpty ? p.riderDisplayName : p.displayHandle; callerName = p.riderDisplayName.isNotEmpty
? p.riderDisplayName
: p.displayHandle;
} catch (_) {} } catch (_) {}
return Container( return Container(
@ -450,7 +474,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
), ),
child: Row( child: Row(
children: [ children: [
const Icon(Icons.warning_amber_rounded, color: Colors.black, size: 22), const Icon(
Icons.warning_amber_rounded,
color: Colors.black,
size: 22,
),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Column( child: Column(
@ -484,7 +512,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
minimumSize: Size.zero, minimumSize: Size.zero,
), ),
onPressed: () => controller.clearRegroup(), onPressed: () => controller.clearRegroup(),
child: const Text('CLEAR', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold)), child: const Text(
'CLEAR',
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
),
) )
else else
TextButton( TextButton(
@ -495,7 +526,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
minimumSize: Size.zero, minimumSize: Size.zero,
), ),
onPressed: () => controller.triggerRegroup(), onPressed: () => controller.triggerRegroup(),
child: const Text('ACK', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold)), child: const Text(
'ACK',
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
),
), ),
], ],
), ),
@ -530,9 +564,13 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
minimumSize: Size.zero, minimumSize: Size.zero,
), ),
onPressed: () { onPressed: () {
ref.read(groupRideLiveSharingAllowedProvider.notifier).state = true; ref.read(groupRideLiveSharingAllowedProvider.notifier).state =
true;
}, },
child: const Text('ENABLE', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold)), child: const Text(
'ENABLE',
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
),
), ),
], ],
), ),
@ -573,9 +611,12 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
final isSelected = currentStatus == status; final isSelected = currentStatus == status;
Color activeColor = const Color(0xFF00E5FF); Color activeColor = const Color(0xFF00E5FF);
if (status == LivePresenceStatus.needFuel) activeColor = Colors.orangeAccent; if (status == LivePresenceStatus.needFuel)
if (status == LivePresenceStatus.regrouping) activeColor = Colors.amber; activeColor = Colors.orangeAccent;
if (status == LivePresenceStatus.arrived) activeColor = Colors.greenAccent; if (status == LivePresenceStatus.regrouping)
activeColor = Colors.amber;
if (status == LivePresenceStatus.arrived)
activeColor = Colors.greenAccent;
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
@ -605,7 +646,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
label, label,
style: TextStyle( style: TextStyle(
color: isSelected ? activeColor : Colors.white70, color: isSelected ? activeColor : Colors.white70,
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500, fontWeight: isSelected
? FontWeight.bold
: FontWeight.w500,
fontSize: 11, fontSize: 11,
), ),
), ),
@ -662,17 +705,17 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
Text( Text(
broadcastState.isBroadcasting broadcastState.isBroadcasting
? (broadcastState.isPaused ? (broadcastState.isPaused
? 'SHARING PAUSED' ? 'SHARING PAUSED'
: (broadcastState.isBackground : (broadcastState.isBackground
? 'BACKGROUND SHARING' ? 'BACKGROUND SHARING'
: 'SHARING LIVE')) : 'SHARING LIVE'))
: 'GHOST MODE', : 'GHOST MODE',
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: broadcastState.isBroadcasting color: broadcastState.isBroadcasting
? (broadcastState.isPaused ? (broadcastState.isPaused
? Colors.amber ? Colors.amber
: const Color(0xFF00E5FF)) : const Color(0xFF00E5FF))
: Colors.white60, : Colors.white60,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 11, fontSize: 11,
@ -682,18 +725,22 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
Text( Text(
broadcastState.isBroadcasting broadcastState.isBroadcasting
? (broadcastState.isPaused ? (broadcastState.isPaused
? 'Reconnecting (${broadcastState.retryCount})...' ? 'Reconnecting (${broadcastState.retryCount})...'
: (broadcastState.isBackground : (broadcastState.isBackground
? 'Active via foreground service' ? 'Active via foreground service'
: 'Broadcasting to riders')) : 'Broadcasting to riders'))
: 'Location paused', : 'Location paused',
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.white38, fontSize: 10), style: const TextStyle(
color: Colors.white38,
fontSize: 10,
),
), ),
], ],
), ),
), ),
Switch( Switch(
key: const ValueKey('live_ride_broadcast_switch'),
value: broadcastState.isOptedIn, value: broadcastState.isOptedIn,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
activeThumbColor: const Color(0xFF00E5FF), activeThumbColor: const Color(0xFF00E5FF),
@ -707,12 +754,17 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
// Recenter Map Button // Recenter Map Button
IconButton( IconButton(
key: const ValueKey('live_ride_recenter_button'),
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: Colors.white10, backgroundColor: Colors.white10,
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
minimumSize: const Size(36, 36), minimumSize: const Size(36, 36),
), ),
icon: const Icon(Icons.my_location, color: Colors.white70, size: 18), icon: const Icon(
Icons.my_location,
color: Colors.white70,
size: 18,
),
tooltip: 'Center on Me', tooltip: 'Center on Me',
onPressed: () { onPressed: () {
if (broadcastState.lastPosition != null) { if (broadcastState.lastPosition != null) {
@ -738,6 +790,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
// Roster Opener Button // Roster Opener Button
ElevatedButton.icon( ElevatedButton.icon(
key: const ValueKey('live_ride_roster_button'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15), backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15),
foregroundColor: const Color(0xFF00E5FF), foregroundColor: const Color(0xFF00E5FF),
@ -877,7 +930,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
participant.riderDisplayName.isNotEmpty participant.riderDisplayName.isNotEmpty
? participant.riderDisplayName[0].toUpperCase() ? participant.riderDisplayName[0].toUpperCase()
: '?', : '?',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
), ),
), ),
if (isLive) if (isLive)
@ -908,7 +964,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
: participant.displayHandle, : participant.displayHandle,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: isCaller ? const Color(0xFF00E5FF) : Colors.white, color: isCaller
? const Color(0xFF00E5FF)
: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 13, fontSize: 13,
), ),
@ -917,9 +975,14 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
if (participant.role != GroupRideRole.participant) ...[ if (participant.role != GroupRideRole.participant) ...[
const SizedBox(width: 6), const SizedBox(width: 6),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), padding: const EdgeInsets.symmetric(
horizontal: 4,
vertical: 1,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _roleColor(participant.role).withValues(alpha: 0.2), color: _roleColor(
participant.role,
).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
), ),
child: Text( child: Text(
@ -954,10 +1017,16 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
), ),
if (distanceString.isNotEmpty) ...[ if (distanceString.isNotEmpty) ...[
const Text(' • ', style: TextStyle(color: Colors.white24, fontSize: 10)), const Text(
' • ',
style: TextStyle(color: Colors.white24, fontSize: 10),
),
Text( Text(
distanceString, distanceString,
style: const TextStyle(color: Colors.white60, fontSize: 10), style: const TextStyle(
color: Colors.white60,
fontSize: 10,
),
), ),
], ],
], ],
@ -980,11 +1049,16 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
IconButton( IconButton(
icon: const Icon(Icons.gps_fixed, size: 16, color: Color(0xFF00E5FF)), icon: const Icon(
Icons.gps_fixed,
size: 16,
color: Color(0xFF00E5FF),
),
constraints: const BoxConstraints(minWidth: 32, minHeight: 32), constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
tooltip: 'Center on rider', tooltip: 'Center on rider',
onPressed: () => onCenter(AppLatLng(presence.latitude, presence.longitude)), onPressed: () =>
onCenter(AppLatLng(presence.latitude, presence.longitude)),
), ),
] else ] else
const Text( const Text(
@ -1029,7 +1103,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
} }
LiveGroupPresence? _findPresence( LiveGroupPresence? _findPresence(
List<LiveGroupPresence> presences, String uid) { List<LiveGroupPresence> presences,
String uid,
) {
try { try {
return presences.firstWhere((p) => p.userId == uid); return presences.firstWhere((p) => p.userId == uid);
} catch (_) { } catch (_) {
@ -1044,7 +1120,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
required GroupRideLocation meetup, required GroupRideLocation meetup,
}) { }) {
if (customCenter != null) return customCenter; if (customCenter != null) return customCenter;
if (localPos != null) return AppLatLng(localPos.latitude, localPos.longitude); if (localPos != null)
return AppLatLng(localPos.latitude, localPos.longitude);
if (leadPresence != null) { if (leadPresence != null) {
return AppLatLng(leadPresence.latitude, leadPresence.longitude); return AppLatLng(leadPresence.latitude, leadPresence.longitude);
} }
@ -1066,7 +1143,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
markers.add( markers.add(
AppMapMarker( AppMapMarker(
id: 'meetup_point', id: 'meetup_point',
position: AppLatLng(ride.meetingPoint.latitude, ride.meetingPoint.longitude), position: AppLatLng(
ride.meetingPoint.latitude,
ride.meetingPoint.longitude,
),
label: ride.meetingPoint.displayName.isNotEmpty label: ride.meetingPoint.displayName.isNotEmpty
? ride.meetingPoint.displayName ? ride.meetingPoint.displayName
: 'Meetup', : 'Meetup',
@ -1076,7 +1156,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
color: Colors.greenAccent, color: Colors.greenAccent,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)), BoxShadow(
color: Colors.black45,
blurRadius: 4,
offset: Offset(0, 2),
),
], ],
), ),
child: const Icon(Icons.flag, color: Colors.black, size: 16), child: const Icon(Icons.flag, color: Colors.black, size: 16),
@ -1106,7 +1190,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
color: Color(0xFF00E5FF), color: Color(0xFF00E5FF),
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)), BoxShadow(
color: Colors.black45,
blurRadius: 4,
offset: Offset(0, 2),
),
], ],
), ),
child: Text( child: Text(
@ -1136,14 +1224,20 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
AppMapMarker( AppMapMarker(
id: 'stop_$i', id: 'stop_$i',
position: AppLatLng(stop.latitude, stop.longitude), position: AppLatLng(stop.latitude, stop.longitude),
label: stop.displayName.isNotEmpty ? stop.displayName : 'Stop ${i + 1}', label: stop.displayName.isNotEmpty
? stop.displayName
: 'Stop ${i + 1}',
iconWidget: Container( iconWidget: Container(
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(5),
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Colors.blueAccent, color: Colors.blueAccent,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)), BoxShadow(
color: Colors.black45,
blurRadius: 4,
offset: Offset(0, 2),
),
], ],
), ),
child: Text( child: Text(
@ -1173,7 +1267,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
markers.add( markers.add(
AppMapMarker( AppMapMarker(
id: 'destination_point', id: 'destination_point',
position: AppLatLng(ride.destination!.latitude, ride.destination!.longitude), position: AppLatLng(
ride.destination!.latitude,
ride.destination!.longitude,
),
label: ride.destination!.displayName.isNotEmpty label: ride.destination!.displayName.isNotEmpty
? ride.destination!.displayName ? ride.destination!.displayName
: 'Destination', : 'Destination',
@ -1183,10 +1280,18 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
color: Colors.redAccent, color: Colors.redAccent,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)), BoxShadow(
color: Colors.black45,
blurRadius: 4,
offset: Offset(0, 2),
),
], ],
), ),
child: const Icon(Icons.sports_score, color: Colors.white, size: 16), child: const Icon(
Icons.sports_score,
color: Colors.white,
size: 16,
),
), ),
), ),
); );
@ -1199,7 +1304,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
p = participants.firstWhere((item) => item.userId == presence.userId); p = participants.firstWhere((item) => item.userId == presence.userId);
} catch (_) {} } catch (_) {}
final isLead = presence.userId == leadUid || p?.role == GroupRideRole.lead; final isLead =
presence.userId == leadUid || p?.role == GroupRideRole.lead;
final isSweep = p?.role == GroupRideRole.sweep; final isSweep = p?.role == GroupRideRole.sweep;
final isCaller = presence.userId == currentUid; final isCaller = presence.userId == currentUid;
@ -1207,8 +1313,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
Color markerColor = const Color(0xFF00E5FF); Color markerColor = const Color(0xFF00E5FF);
if (isLead) markerColor = const Color(0xFFFFD700); // Gold for lead if (isLead) markerColor = const Color(0xFFFFD700); // Gold for lead
if (isSweep) markerColor = Colors.orangeAccent; // Orange for sweep if (isSweep) markerColor = Colors.orangeAccent; // Orange for sweep
if (presence.status == LivePresenceStatus.regrouping) markerColor = Colors.amber; if (presence.status == LivePresenceStatus.regrouping)
if (presence.status == LivePresenceStatus.needFuel) markerColor = Colors.deepOrangeAccent; markerColor = Colors.amber;
if (presence.status == LivePresenceStatus.needFuel)
markerColor = Colors.deepOrangeAccent;
final isStale = presence.isStale; final isStale = presence.isStale;
final opacity = isStale ? 0.4 : 1.0; final opacity = isStale ? 0.4 : 1.0;
@ -1256,14 +1364,19 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
} }
void _confirmEndRide( void _confirmEndRide(
BuildContext context, GroupRide ride, String? currentUid) { BuildContext context,
GroupRide ride,
String? currentUid,
) {
if (currentUid == null) return; if (currentUid == null) return;
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
backgroundColor: const Color(0xFF161922), backgroundColor: const Color(0xFF161922),
title: const Text('End Group Ride?', title: const Text(
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), 'End Group Ride?',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
content: const Text( content: const Text(
'This will conclude the group ride for all participants and terminate live location sharing.', 'This will conclude the group ride for all participants and terminate live location sharing.',
style: TextStyle(color: Colors.white70), style: TextStyle(color: Colors.white70),
@ -1271,7 +1384,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx), onPressed: () => Navigator.pop(ctx),
child: const Text('CANCEL', style: TextStyle(color: Colors.white60)), child: const Text(
'CANCEL',
style: TextStyle(color: Colors.white60),
),
), ),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(

View File

@ -13,7 +13,8 @@ class HistoryScreen extends ConsumerStatefulWidget {
} }
class _HistoryScreenState extends ConsumerState<HistoryScreen> { class _HistoryScreenState extends ConsumerState<HistoryScreen> {
int? _selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID int?
_selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -42,9 +43,14 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
// Filter logic // Filter logic
if (_selectedMotorcycleId == -1 && defaultBike != null) { if (_selectedMotorcycleId == -1 && defaultBike != null) {
rides = rides.where((r) => r.motorcycleId == defaultBike.id).toList(); rides = rides
} else if (_selectedMotorcycleId != null && _selectedMotorcycleId! > 0) { .where((r) => r.motorcycleId == defaultBike.id)
rides = rides.where((r) => r.motorcycleId == _selectedMotorcycleId).toList(); .toList();
} else if (_selectedMotorcycleId != null &&
_selectedMotorcycleId! > 0) {
rides = rides
.where((r) => r.motorcycleId == _selectedMotorcycleId)
.toList();
} }
// Sort by start time descending // Sort by start time descending
@ -64,14 +70,21 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
itemCount: rides.length, itemCount: rides.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final ride = rides[index]; final ride = rides[index];
final bikeName = (ride.motorcycleYear != null && ride.motorcycleMake != null && ride.motorcycleModel != null) final bikeName =
(ride.motorcycleYear != null &&
ride.motorcycleMake != null &&
ride.motorcycleModel != null)
? '${ride.motorcycleYear} ${ride.motorcycleMake} ${ride.motorcycleModel}' ? '${ride.motorcycleYear} ${ride.motorcycleMake} ${ride.motorcycleModel}'
: null; : null;
return Card( return Card(
elevation: 4, elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), margin: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 4,
),
child: InkWell( child: InkWell(
key: ValueKey('history_ride_card_${ride.id}'),
onTap: () { onTap: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
@ -92,23 +105,39 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
children: [ children: [
Text( Text(
'Ride ${ride.id}', 'Ride ${ride.id}',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
), ),
if (bikeName != null) ...[ if (bikeName != null) ...[
const SizedBox(width: 8), const SizedBox(width: 8),
Flexible( Flexible(
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), padding:
const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.cyanAccent.withValues(alpha: 0.15), color: Colors.cyanAccent
borderRadius: BorderRadius.circular(4), .withValues(alpha: 0.15),
border: Border.all(color: Colors.cyanAccent.withValues(alpha: 0.5)), borderRadius:
BorderRadius.circular(4),
border: Border.all(
color: Colors.cyanAccent
.withValues(alpha: 0.5),
),
), ),
child: Text( child: Text(
bikeName, bikeName,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.cyanAccent), style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Colors.cyanAccent,
),
), ),
), ),
), ),
@ -118,27 +147,52 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
ride.startTime.toLocal().toString().split('.')[0], ride.startTime.toLocal().toString().split(
style: const TextStyle(color: Colors.grey, fontSize: 11), '.',
)[0],
style: const TextStyle(
color: Colors.grey,
fontSize: 11,
),
), ),
], ],
), ),
const Divider(), const Divider(),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [ children: [
_buildStatCol('TRIP', '${ride.distanceMiles.toStringAsFixed(1)} mi'), _buildStatCol(
_buildStatCol('TIME', '${(ride.movingTime / 60).toStringAsFixed(0)} min'), 'TRIP',
_buildStatCol('TOP SPD', '${ride.topSpeedMph.toStringAsFixed(0)} mph'), '${ride.distanceMiles.toStringAsFixed(1)} mi',
),
_buildStatCol(
'TIME',
'${(ride.movingTime / 60).toStringAsFixed(0)} min',
),
_buildStatCol(
'TOP SPD',
'${ride.topSpeedMph.toStringAsFixed(0)} mph',
),
], ],
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [ children: [
_buildStatCol('MAX LEAN', '${math.max(ride.maxLeanLeft, ride.maxLeanRight).toStringAsFixed(0)}°'), _buildStatCol(
_buildStatCol('HARD BRK', '${ride.maxBrakingG.toStringAsFixed(2)}G'), 'MAX LEAN',
_buildStatCol('ELEV GAIN', '${ride.elevationGain.toStringAsFixed(0)}m'), '${math.max(ride.maxLeanLeft, ride.maxLeanRight).toStringAsFixed(0)}°',
),
_buildStatCol(
'HARD BRK',
'${ride.maxBrakingG.toStringAsFixed(2)}G',
),
_buildStatCol(
'ELEV GAIN',
'${ride.elevationGain.toStringAsFixed(0)}m',
),
], ],
), ),
], ],
@ -156,7 +210,10 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
); );
} }
Widget _buildFilterBar(List<GarageMotorcycle> bikes, GarageMotorcycle? defaultBike) { Widget _buildFilterBar(
List<GarageMotorcycle> bikes,
GarageMotorcycle? defaultBike,
) {
if (bikes.isEmpty) return const SizedBox(); if (bikes.isEmpty) return const SizedBox();
return Padding( return Padding(
@ -188,10 +245,12 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
value: -1, value: -1,
child: Text('Current (${defaultBike.modelName})'), child: Text('Current (${defaultBike.modelName})'),
), ),
...bikes.map((b) => DropdownMenuItem<int?>( ...bikes.map(
value: b.id, (b) => DropdownMenuItem<int?>(
child: Text('${b.year} ${b.makeName} ${b.modelName}'), value: b.id,
)), child: Text('${b.year} ${b.makeName} ${b.modelName}'),
),
),
], ],
onChanged: (val) { onChanged: (val) {
setState(() { setState(() {
@ -212,7 +271,10 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)), Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)),
Text(val, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), Text(
val,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
], ],
); );
} }

View File

@ -29,7 +29,10 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Color(0xFF2A2E3D)), side: const BorderSide(color: Color(0xFF2A2E3D)),
), ),
title: const Text('Delete Route', style: TextStyle(color: Colors.white)), title: const Text(
'Delete Route',
style: TextStyle(color: Colors.white),
),
content: Text( content: Text(
'Are you sure you want to delete "${route.title}"? This cannot be undone.', 'Are you sure you want to delete "${route.title}"? This cannot be undone.',
style: const TextStyle(color: Colors.white70), style: const TextStyle(color: Colors.white70),
@ -37,7 +40,10 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx, false), onPressed: () => Navigator.pop(ctx, false),
child: const Text('CANCEL', style: TextStyle(color: Colors.white54)), child: const Text(
'CANCEL',
style: TextStyle(color: Colors.white54),
),
), ),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -58,17 +64,17 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
final service = ref.read(routeServiceProvider); final service = ref.read(routeServiceProvider);
await service.deleteRoute(widget.routeId); await service.deleteRoute(widget.routeId);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
const SnackBar(content: Text('Route deleted.')), context,
); ).showSnackBar(const SnackBar(content: Text('Route deleted.')));
context.pop(); context.pop();
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
setState(() => _isDeleting = false); setState(() => _isDeleting = false);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text('Failed to delete route: $e')), context,
); ).showSnackBar(SnackBar(content: Text('Failed to delete route: $e')));
} }
} }
} }
@ -132,8 +138,11 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.error_outline, const Icon(
size: 48, color: Color(0xFFFF5252)), Icons.error_outline,
size: 48,
color: Color(0xFFFF5252),
),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
'Could not load route: $err', 'Could not load route: $err',
@ -160,7 +169,8 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
final markers = route.stops.map((stop) { final markers = route.stops.map((stop) {
final isFirst = stop.orderIndex == 0; final isFirst = stop.orderIndex == 0;
final isLast = final isLast =
stop.orderIndex == route.stops.length - 1 && route.stops.length > 1; stop.orderIndex == route.stops.length - 1 &&
route.stops.length > 1;
return AppMapMarker( return AppMapMarker(
id: stop.id, id: stop.id,
@ -172,12 +182,15 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
color: isFirst color: isFirst
? const Color(0xFF00E676) ? const Color(0xFF00E676)
: isLast : isLast
? const Color(0xFFFF5252) ? const Color(0xFFFF5252)
: const Color(0xFF00E5FF), : const Color(0xFF00E5FF),
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
color: Colors.black54, blurRadius: 4, spreadRadius: 1), color: Colors.black54,
blurRadius: 4,
spreadRadius: 1,
),
], ],
), ),
child: Text( child: Text(
@ -216,7 +229,9 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
final mapCenter = route.stops.isNotEmpty final mapCenter = route.stops.isNotEmpty
? AppLatLng( ? AppLatLng(
route.stops.first.latitude, route.stops.first.longitude) route.stops.first.latitude,
route.stops.first.longitude,
)
: const AppLatLng(37.7749, -122.4194); : const AppLatLng(37.7749, -122.4194);
return SingleChildScrollView( return SingleChildScrollView(
@ -240,9 +255,13 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
left: 8, left: 8,
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4), horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF13151A).withValues(alpha: 0.9), color: const Color(
0xFF13151A,
).withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
border: Border.all(color: const Color(0xFF2A2E3D)), border: Border.all(color: const Color(0xFF2A2E3D)),
), ),
@ -370,35 +389,54 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
children: [ children: [
Expanded( Expanded(
child: OutlinedButton.icon( child: OutlinedButton.icon(
key: const ValueKey('route_detail_edit_button'),
onPressed: () { onPressed: () {
context.push('/routes/planner?routeId=${route.id}'); context.push(
'/routes/planner?routeId=${route.id}',
);
}, },
icon: const Icon(Icons.edit, icon: const Icon(
size: 16, color: Color(0xFF00E5FF)), Icons.edit,
label: const Text('EDIT ROUTE', size: 16,
style: TextStyle(color: Color(0xFF00E5FF))), color: Color(0xFF00E5FF),
),
label: const Text(
'EDIT ROUTE',
style: TextStyle(color: Color(0xFF00E5FF)),
),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
side: const BorderSide( side: const BorderSide(
color: Color(0xFF00E5FF)), color: Color(0xFF00E5FF),
),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 10), vertical: 10,
),
), ),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
OutlinedButton.icon( OutlinedButton.icon(
key: const ValueKey('route_detail_delete_button'),
onPressed: _isDeleting onPressed: _isDeleting
? null ? null
: () => _confirmDelete(route), : () => _confirmDelete(route),
icon: const Icon(Icons.delete_outline, icon: const Icon(
size: 16, color: Color(0xFFFF5252)), Icons.delete_outline,
label: const Text('DELETE', size: 16,
style: TextStyle(color: Color(0xFFFF5252))), color: Color(0xFFFF5252),
),
label: const Text(
'DELETE',
style: TextStyle(color: Color(0xFFFF5252)),
),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
side: const BorderSide( side: const BorderSide(
color: Color(0xFFFF5252)), color: Color(0xFFFF5252),
),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 16), vertical: 10,
horizontal: 16,
),
), ),
), ),
], ],
@ -409,14 +447,23 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
// Attached Group Ride button if any // Attached Group Ride button if any
if (route.attachedGroupRideId != null) ...[ if (route.attachedGroupRideId != null) ...[
OutlinedButton.icon( OutlinedButton.icon(
key: const ValueKey(
'route_detail_view_attached_group_ride_button',
),
onPressed: () { onPressed: () {
context.push( context.push(
'/group-rides/${route.attachedGroupRideId}'); '/group-rides/${route.attachedGroupRideId}',
);
}, },
icon: const Icon(Icons.group, icon: const Icon(
size: 18, color: Color(0xFF00E5FF)), Icons.group,
label: const Text('VIEW ATTACHED GROUP RIDE', size: 18,
style: TextStyle(color: Color(0xFF00E5FF))), color: Color(0xFF00E5FF),
),
label: const Text(
'VIEW ATTACHED GROUP RIDE',
style: TextStyle(color: Color(0xFF00E5FF)),
),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFF2A2E3D)), side: const BorderSide(color: Color(0xFF2A2E3D)),
minimumSize: const Size(double.infinity, 44), minimumSize: const Size(double.infinity, 44),
@ -445,7 +492,8 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final stop = route.stops[index]; final stop = route.stops[index];
final isFirst = index == 0; final isFirst = index == 0;
final isLast = index == route.stops.length - 1 && final isLast =
index == route.stops.length - 1 &&
route.stops.length > 1; route.stops.length > 1;
return Container( return Container(
@ -454,7 +502,9 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF13151A), color: const Color(0xFF13151A),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF2A2E3D)), border: Border.all(
color: const Color(0xFF2A2E3D),
),
), ),
child: Row( child: Row(
children: [ children: [
@ -465,8 +515,8 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
color: isFirst color: isFirst
? const Color(0xFF00E676) ? const Color(0xFF00E676)
: isLast : isLast
? const Color(0xFFFF5252) ? const Color(0xFFFF5252)
: const Color(0xFF00E5FF), : const Color(0xFF00E5FF),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
alignment: Alignment.center, alignment: Alignment.center,
@ -487,9 +537,11 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
children: [ children: [
Row( Row(
children: [ children: [
Icon(_getIconForStopType(stop.stopType), Icon(
size: 14, _getIconForStopType(stop.stopType),
color: const Color(0xFF00E5FF)), size: 14,
color: const Color(0xFF00E5FF),
),
const SizedBox(width: 4), const SizedBox(width: 4),
Flexible( Flexible(
child: Text( child: Text(
@ -540,7 +592,9 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
backgroundColor: const Color(0xFF00E5FF), backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black, foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6), horizontal: 10,
vertical: 6,
),
textStyle: const TextStyle( textStyle: const TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -557,6 +611,9 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
// Primary action: Navigate to First Stop // Primary action: Navigate to First Stop
if (route.stops.isNotEmpty) if (route.stops.isNotEmpty)
ElevatedButton.icon( ElevatedButton.icon(
key: const ValueKey(
'route_detail_navigate_start_button',
),
onPressed: () { onPressed: () {
final firstStop = route.stops.first; final firstStop = route.stops.first;
NavigationHandoffService.showHandoffSheet( NavigationHandoffService.showHandoffSheet(

View File

@ -108,7 +108,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
}) { }) {
final state = ref.read(routePlannerNotifierProvider); final state = ref.read(routePlannerNotifierProvider);
final isPro = ref.read(isProSubscriberProvider); final isPro = ref.read(isProSubscriberProvider);
final maxAllowed = isPro ? RouteService.maxTechnicalStops : RouteService.freeStopCap; final maxAllowed = isPro
? RouteService.maxTechnicalStops
: RouteService.freeStopCap;
if (existingStop == null && state.stops.length >= maxAllowed) { if (existingStop == null && state.stops.length >= maxAllowed) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -145,10 +147,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
: (initialLng?.toStringAsFixed(6) ?? ''), : (initialLng?.toStringAsFixed(6) ?? ''),
); );
final notesCtrl = TextEditingController(text: existingStop?.notes ?? ''); final notesCtrl = TextEditingController(text: existingStop?.notes ?? '');
RouteStopType selectedType = existingStop?.stopType ?? RouteStopType selectedType =
(state.stops.isEmpty existingStop?.stopType ??
? RouteStopType.origin (state.stops.isEmpty ? RouteStopType.origin : RouteStopType.waypoint);
: RouteStopType.waypoint);
showDialog( showDialog(
context: context, context: context,
@ -206,8 +207,11 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
value: type, value: type,
child: Row( child: Row(
children: [ children: [
Icon(_getIconForStopType(type), Icon(
color: const Color(0xFF00E5FF), size: 18), _getIconForStopType(type),
color: const Color(0xFF00E5FF),
size: 18,
),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
@ -235,7 +239,10 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
decimal: true, decimal: true,
signed: true, signed: true,
), ),
style: const TextStyle(color: Colors.white, fontSize: 13), style: const TextStyle(
color: Colors.white,
fontSize: 13,
),
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Latitude *', labelText: 'Latitude *',
labelStyle: TextStyle(color: Colors.white60), labelStyle: TextStyle(color: Colors.white60),
@ -252,7 +259,10 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
decimal: true, decimal: true,
signed: true, signed: true,
), ),
style: const TextStyle(color: Colors.white, fontSize: 13), style: const TextStyle(
color: Colors.white,
fontSize: 13,
),
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Longitude *', labelText: 'Longitude *',
labelStyle: TextStyle(color: Colors.white60), labelStyle: TextStyle(color: Colors.white60),
@ -281,7 +291,10 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(dialogCtx), onPressed: () => Navigator.pop(dialogCtx),
child: const Text('CANCEL', style: TextStyle(color: Colors.white54)), child: const Text(
'CANCEL',
style: TextStyle(color: Colors.white54),
),
), ),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -295,24 +308,32 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
if (name.isEmpty) { if (name.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please provide a stop name.')), const SnackBar(
content: Text('Please provide a stop name.'),
),
); );
return; return;
} }
if (lat == null || lat < -90.0 || lat > 90.0) { if (lat == null || lat < -90.0 || lat > 90.0) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invalid latitude (-90 to 90).')), const SnackBar(
content: Text('Invalid latitude (-90 to 90).'),
),
); );
return; return;
} }
if (lng == null || lng < -180.0 || lng > 180.0) { if (lng == null || lng < -180.0 || lng > 180.0) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invalid longitude (-180 to 180).')), const SnackBar(
content: Text('Invalid longitude (-180 to 180).'),
),
); );
return; return;
} }
final notifier = ref.read(routePlannerNotifierProvider.notifier); final notifier = ref.read(
routePlannerNotifierProvider.notifier,
);
if (existingStop != null) { if (existingStop != null) {
notifier.updateStop( notifier.updateStop(
existingStop.copyWith( existingStop.copyWith(
@ -362,7 +383,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
final isPro = ref.read(isProSubscriberProvider); final isPro = ref.read(isProSubscriberProvider);
final state = ref.read(routePlannerNotifierProvider); final state = ref.read(routePlannerNotifierProvider);
final maxAllowed = isPro ? RouteService.maxTechnicalStops : RouteService.freeStopCap; final maxAllowed = isPro
? RouteService.maxTechnicalStops
: RouteService.freeStopCap;
if (state.stops.length > maxAllowed) { if (state.stops.length > maxAllowed) {
setState(() { setState(() {
@ -447,13 +470,17 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
if (!_initialized) { if (!_initialized) {
return const Scaffold( return const Scaffold(
backgroundColor: Color(0xFF0D0E11), backgroundColor: Color(0xFF0D0E11),
body: Center(child: CircularProgressIndicator(color: Color(0xFF00E5FF))), body: Center(
child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
),
); );
} }
final state = ref.watch(routePlannerNotifierProvider); final state = ref.watch(routePlannerNotifierProvider);
final isPro = ref.watch(isProSubscriberProvider); final isPro = ref.watch(isProSubscriberProvider);
final maxAllowed = isPro ? RouteService.maxTechnicalStops : RouteService.freeStopCap; final maxAllowed = isPro
? RouteService.maxTechnicalStops
: RouteService.freeStopCap;
final isAtLimit = state.stops.length >= maxAllowed; final isAtLimit = state.stops.length >= maxAllowed;
// Center map on first stop or default // Center map on first stop or default
@ -464,7 +491,8 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
// Build markers // Build markers
final markers = state.stops.map((stop) { final markers = state.stops.map((stop) {
final isFirst = stop.orderIndex == 0; final isFirst = stop.orderIndex == 0;
final isLast = stop.orderIndex == state.stops.length - 1 && state.stops.length > 1; final isLast =
stop.orderIndex == state.stops.length - 1 && state.stops.length > 1;
return AppMapMarker( return AppMapMarker(
id: stop.id, id: stop.id,
@ -476,8 +504,8 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
color: isFirst color: isFirst
? const Color(0xFF00E676) ? const Color(0xFF00E676)
: isLast : isLast
? const Color(0xFFFF5252) ? const Color(0xFFFF5252)
: const Color(0xFF00E5FF), : const Color(0xFF00E5FF),
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: const [ boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 4, spreadRadius: 1), BoxShadow(color: Colors.black54, blurRadius: 4, spreadRadius: 1),
@ -551,6 +579,7 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
) )
else else
TextButton.icon( TextButton.icon(
key: const ValueKey('route_planner_save_button'),
onPressed: _saveRoute, onPressed: _saveRoute,
icon: const Icon(Icons.save, color: Color(0xFF00E5FF), size: 18), icon: const Icon(Icons.save, color: Color(0xFF00E5FF), size: 18),
label: const Text( label: const Text(
@ -574,11 +603,17 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
Container( Container(
width: double.infinity, width: double.infinity,
color: const Color(0xFFFF5252).withValues(alpha: 0.15), color: const Color(0xFFFF5252).withValues(alpha: 0.15),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Row( child: Row(
children: [ children: [
const Icon(Icons.error_outline, const Icon(
color: Color(0xFFFF5252), size: 18), Icons.error_outline,
color: Color(0xFFFF5252),
size: 18,
),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
@ -590,7 +625,11 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
), ),
), ),
IconButton( IconButton(
icon: const Icon(Icons.close, color: Colors.white54, size: 16), icon: const Icon(
Icons.close,
color: Colors.white54,
size: 16,
),
onPressed: () => setState(() => _errorMessage = null), onPressed: () => setState(() => _errorMessage = null),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(), constraints: const BoxConstraints(),
@ -618,7 +657,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
right: 8, right: 8,
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6), horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF13151A).withValues(alpha: 0.9), color: const Color(0xFF13151A).withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@ -643,11 +684,17 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
const SizedBox(width: 8), const SizedBox(width: 8),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2), horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isAtLimit color: isAtLimit
? const Color(0xFFFFB300).withValues(alpha: 0.2) ? const Color(
: const Color(0xFF00E5FF).withValues(alpha: 0.15), 0xFFFFB300,
).withValues(alpha: 0.2)
: const Color(
0xFF00E5FF,
).withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: Text( child: Text(
@ -700,7 +747,10 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
), ),
decoration: const InputDecoration( decoration: const InputDecoration(
hintText: 'Route Title (e.g. Skyline Ridge Loop) *', hintText: 'Route Title (e.g. Skyline Ridge Loop) *',
hintStyle: TextStyle(color: Colors.white30, fontSize: 14), hintStyle: TextStyle(
color: Colors.white30,
fontSize: 14,
),
isDense: true, isDense: true,
contentPadding: EdgeInsets.symmetric(vertical: 8), contentPadding: EdgeInsets.symmetric(vertical: 8),
border: InputBorder.none, border: InputBorder.none,
@ -721,8 +771,11 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.visibility_outlined, const Icon(
size: 14, color: Colors.white54), Icons.visibility_outlined,
size: 14,
color: Colors.white54,
),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
_visibility.label, _visibility.label,
@ -732,30 +785,36 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
const Icon(Icons.arrow_drop_down, const Icon(
size: 16, color: Color(0xFF00E5FF)), Icons.arrow_drop_down,
size: 16,
color: Color(0xFF00E5FF),
),
], ],
), ),
), ),
itemBuilder: (context) => RouteVisibility.values.map((v) { itemBuilder: (context) =>
return PopupMenuItem( RouteVisibility.values.map((v) {
value: v, return PopupMenuItem(
child: Text( value: v,
v.label, child: Text(
style: const TextStyle( v.label,
color: Colors.white, style: const TextStyle(
fontSize: 13, color: Colors.white,
), fontSize: 13,
), ),
); ),
}).toList(), );
}).toList(),
), ),
if (!isPro && isAtLimit) if (!isPro && isAtLimit)
TextButton( TextButton(
onPressed: () => context.push('/settings/pro'), onPressed: () => context.push('/settings/pro'),
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4), horizontal: 8,
vertical: 4,
),
minimumSize: Size.zero, minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap, tapTargetSize: MaterialTapTargetSize.shrinkWrap,
), ),
@ -778,7 +837,10 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
// Stop List Controls (Add Stop / Add GPS / Reverse) // Stop List Controls (Add Stop / Add GPS / Reverse)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Wrap( child: Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
@ -789,6 +851,7 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ElevatedButton.icon( ElevatedButton.icon(
key: const ValueKey('route_planner_add_stop_button'),
onPressed: () => _showAddOrEditStopDialog(), onPressed: () => _showAddOrEditStopDialog(),
icon: const Icon(Icons.add_location_alt, size: 16), icon: const Icon(Icons.add_location_alt, size: 16),
label: Text(isNarrow ? 'ADD' : 'ADD STOP'), label: Text(isNarrow ? 'ADD' : 'ADD STOP'),
@ -796,7 +859,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
backgroundColor: const Color(0xFF00E5FF), backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black, foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 8), horizontal: 10,
vertical: 8,
),
textStyle: const TextStyle( textStyle: const TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -805,15 +870,25 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
OutlinedButton.icon( OutlinedButton.icon(
key: const ValueKey(
'route_planner_current_gps_button',
),
onPressed: _addCurrentLocationStop, onPressed: _addCurrentLocationStop,
icon: const Icon(Icons.my_location, icon: const Icon(
size: 16, color: Color(0xFF00E5FF)), Icons.my_location,
label: Text(isNarrow ? 'GPS' : 'CURRENT GPS', size: 16,
style: const TextStyle(color: Color(0xFF00E5FF))), color: Color(0xFF00E5FF),
),
label: Text(
isNarrow ? 'GPS' : 'CURRENT GPS',
style: const TextStyle(color: Color(0xFF00E5FF)),
),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFF2A2E3D)), side: const BorderSide(color: Color(0xFF2A2E3D)),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 8), horizontal: 10,
vertical: 8,
),
textStyle: const TextStyle(fontSize: 12), textStyle: const TextStyle(fontSize: 12),
), ),
), ),
@ -821,15 +896,24 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
), ),
if (state.stops.length >= 2) if (state.stops.length >= 2)
TextButton.icon( TextButton.icon(
onPressed: () => key: const ValueKey('route_planner_reverse_button'),
ref.read(routePlannerNotifierProvider.notifier).reverseStops(), onPressed: () => ref
icon: const Icon(Icons.swap_vert, .read(routePlannerNotifierProvider.notifier)
size: 16, color: Colors.white70), .reverseStops(),
label: const Text('REVERSE', icon: const Icon(
style: TextStyle(color: Colors.white70, fontSize: 11)), Icons.swap_vert,
size: 16,
color: Colors.white70,
),
label: const Text(
'REVERSE',
style: TextStyle(color: Colors.white70, fontSize: 11),
),
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 4), horizontal: 6,
vertical: 4,
),
minimumSize: Size.zero, minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap, tapTargetSize: MaterialTapTargetSize.shrinkWrap,
), ),
@ -845,27 +929,35 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.alt_route, Icon(
size: 48, Icons.alt_route,
color: Colors.white.withValues(alpha: 0.15)), size: 48,
color: Colors.white.withValues(alpha: 0.15),
),
const SizedBox(height: 12), const SizedBox(height: 12),
const Text( const Text(
'No stops added yet.', 'No stops added yet.',
style: TextStyle( style: TextStyle(
color: Colors.white54, fontSize: 14), color: Colors.white54,
fontSize: 14,
),
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
const Text( const Text(
'Tap "Add Stop" or "Current GPS" to begin planning.', 'Tap "Add Stop" or "Current GPS" to begin planning.',
style: TextStyle( style: TextStyle(
color: Colors.white30, fontSize: 12), color: Colors.white30,
fontSize: 12,
),
), ),
], ],
), ),
) )
: ReorderableListView.builder( : ReorderableListView.builder(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 4), horizontal: 16,
vertical: 4,
),
itemCount: state.stops.length, itemCount: state.stops.length,
// ignore: deprecated_member_use // ignore: deprecated_member_use
onReorder: (oldIdx, newIdx) { onReorder: (oldIdx, newIdx) {
@ -877,7 +969,8 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
final stop = state.stops[index]; final stop = state.stops[index];
final isFirst = index == 0; final isFirst = index == 0;
final isLast = final isLast =
index == state.stops.length - 1 && state.stops.length > 1; index == state.stops.length - 1 &&
state.stops.length > 1;
return Container( return Container(
key: ValueKey(stop.id), key: ValueKey(stop.id),
@ -886,7 +979,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF13151A), color: const Color(0xFF13151A),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFF2A2E3D)), border: Border.all(
color: const Color(0xFF2A2E3D),
),
), ),
child: Row( child: Row(
children: [ children: [
@ -898,8 +993,8 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
color: isFirst color: isFirst
? const Color(0xFF00E676) ? const Color(0xFF00E676)
: isLast : isLast
? const Color(0xFFFF5252) ? const Color(0xFFFF5252)
: const Color(0xFF00E5FF), : const Color(0xFF00E5FF),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
alignment: Alignment.center, alignment: Alignment.center,
@ -917,7 +1012,8 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
// Stop Details // Stop Details
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment:
CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
@ -966,8 +1062,11 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
// Actions: Handoff navigation, Edit, Delete // Actions: Handoff navigation, Edit, Delete
IconButton( IconButton(
icon: const Icon(Icons.navigation, icon: const Icon(
color: Color(0xFF00E5FF), size: 18), Icons.navigation,
color: Color(0xFF00E5FF),
size: 18,
),
tooltip: 'Open in Maps', tooltip: 'Open in Maps',
onPressed: () { onPressed: () {
NavigationHandoffService.showHandoffSheet( NavigationHandoffService.showHandoffSheet(
@ -978,33 +1077,50 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
); );
}, },
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32), constraints: const BoxConstraints(
minWidth: 32,
),
), ),
IconButton( IconButton(
icon: const Icon(Icons.edit, icon: const Icon(
color: Colors.white60, size: 18), Icons.edit,
color: Colors.white60,
size: 18,
),
tooltip: 'Edit stop', tooltip: 'Edit stop',
onPressed: () => _showAddOrEditStopDialog( onPressed: () => _showAddOrEditStopDialog(
existingStop: stop, existingStop: stop,
), ),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32), constraints: const BoxConstraints(
minWidth: 32,
),
), ),
IconButton( IconButton(
icon: const Icon(Icons.delete_outline, icon: const Icon(
color: Color(0xFFFF5252), size: 18), Icons.delete_outline,
color: Color(0xFFFF5252),
size: 18,
),
tooltip: 'Remove stop', tooltip: 'Remove stop',
onPressed: () { onPressed: () {
ref ref
.read(routePlannerNotifierProvider.notifier) .read(
routePlannerNotifierProvider.notifier,
)
.removeStop(stop.id); .removeStop(stop.id);
}, },
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32), constraints: const BoxConstraints(
minWidth: 32,
),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
const Icon(Icons.drag_handle, const Icon(
color: Colors.white30, size: 20), Icons.drag_handle,
color: Colors.white30,
size: 20,
),
], ],
), ),
); );

View File

@ -89,6 +89,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
final user = authState.user; final user = authState.user;
return InkWell( return InkWell(
key: const ValueKey('settings_account_card'),
onTap: () => context.go('/settings/account'), onTap: () => context.go('/settings/account'),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),

View File

@ -174,6 +174,7 @@ class CurrentPlanCard extends ConsumerWidget {
if (!isPro) ...[ if (!isPro) ...[
Expanded( Expanded(
child: ElevatedButton( child: ElevatedButton(
key: const ValueKey('settings_upgrade_to_pro_button'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00E5FF), backgroundColor: const Color(0xFF00E5FF),
foregroundColor: Colors.black, foregroundColor: Colors.black,
@ -218,6 +219,7 @@ class CurrentPlanCard extends ConsumerWidget {
const SizedBox(width: 10), const SizedBox(width: 10),
], ],
OutlinedButton( OutlinedButton(
key: const ValueKey('settings_pro_details_button'),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: Colors.white70, foregroundColor: Colors.white70,
side: const BorderSide(color: Colors.white24), side: const BorderSide(color: Colors.white24),

View File

@ -1,5 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:marionette_flutter/marionette_flutter.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:app_links/app_links.dart'; import 'package:app_links/app_links.dart';
@ -11,7 +14,13 @@ import 'features/records/records_service.dart';
import 'services/sync/sync_observer.dart'; import 'services/sync/sync_observer.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); final isFlutterTest =
!kIsWeb && Platform.environment.containsKey('FLUTTER_TEST');
if (kDebugMode && !isFlutterTest) {
MarionetteBinding.ensureInitialized();
} else {
WidgetsFlutterBinding.ensureInitialized();
}
try { try {
await Firebase.initializeApp( await Firebase.initializeApp(
@ -68,7 +77,9 @@ class _MotorcycleDashAppState extends ConsumerState<MotorcycleDashApp> {
void _handleDeepLink(Uri uri) { void _handleDeepLink(Uri uri) {
final uriStr = uri.toString(); final uriStr = uri.toString();
if (uriStr.contains('callback') || uri.host == 'oauth' || uri.scheme == 'motorsignal') { if (uriStr.contains('callback') ||
uri.host == 'oauth' ||
uri.scheme == 'motorsignal') {
try { try {
appRouter.go('/oauth/callback?${uri.query}'); appRouter.go('/oauth/callback?${uri.query}');
} catch (_) {} } catch (_) {}

View File

@ -32,8 +32,11 @@ class NavigationHandoffService {
required double longitude, required double longitude,
String? displayName, String? displayName,
}) { }) {
final dest = '${latitude.toStringAsFixed(6)},${longitude.toStringAsFixed(6)}'; final dest =
return Uri.parse('https://www.google.com/maps/dir/?api=1&destination=$dest'); '${latitude.toStringAsFixed(6)},${longitude.toStringAsFixed(6)}';
return Uri.parse(
'https://www.google.com/maps/dir/?api=1&destination=$dest',
);
} }
/// Builds a standard Waze navigation URI. /// Builds a standard Waze navigation URI.
@ -51,7 +54,8 @@ class NavigationHandoffService {
required double longitude, required double longitude,
required String displayName, required String displayName,
}) { }) {
final coords = '${latitude.toStringAsFixed(6)},${longitude.toStringAsFixed(6)}'; final coords =
'${latitude.toStringAsFixed(6)},${longitude.toStringAsFixed(6)}';
return Uri.parse('geo:$coords?q=$coords($displayName)'); return Uri.parse('geo:$coords?q=$coords($displayName)');
} }
@ -79,7 +83,11 @@ class NavigationHandoffService {
children: [ children: [
Row( Row(
children: [ children: [
const Icon(Icons.navigation, color: Color(0xFF00E5FF), size: 22), const Icon(
Icons.navigation,
color: Color(0xFF00E5FF),
size: 22,
),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
@ -112,6 +120,7 @@ class NavigationHandoffService {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
ListTile( ListTile(
key: const ValueKey('handoff_google_maps_tile'),
leading: Container( leading: Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -120,9 +129,22 @@ class NavigationHandoffService {
), ),
child: const Icon(Icons.map, color: Colors.white), child: const Icon(Icons.map, color: Colors.white),
), ),
title: const Text('Google Maps', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)), title: const Text(
subtitle: const Text('Open turn-by-turn in Google Maps', style: TextStyle(color: Colors.white54, fontSize: 12)), 'Google Maps',
trailing: const Icon(Icons.open_in_new, color: Colors.white38, size: 18), style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
subtitle: const Text(
'Open turn-by-turn in Google Maps',
style: TextStyle(color: Colors.white54, fontSize: 12),
),
trailing: const Icon(
Icons.open_in_new,
color: Colors.white38,
size: 18,
),
onTap: () async { onTap: () async {
Navigator.pop(sheetContext); Navigator.pop(sheetContext);
final uri = buildGoogleMapsUri( final uri = buildGoogleMapsUri(
@ -131,23 +153,43 @@ class NavigationHandoffService {
displayName: displayName, displayName: displayName,
); );
if (await canLaunchUrl(uri)) { if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication); await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
} }
}, },
), ),
const Divider(color: Colors.white12, height: 1), const Divider(color: Colors.white12, height: 1),
ListTile( ListTile(
key: const ValueKey('handoff_waze_tile'),
leading: Container( leading: Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white10, color: Colors.white10,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Icon(Icons.directions_car, color: Color(0xFF00E5FF)), child: const Icon(
Icons.directions_car,
color: Color(0xFF00E5FF),
),
),
title: const Text(
'Waze',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
subtitle: const Text(
'Open road directions in Waze',
style: TextStyle(color: Colors.white54, fontSize: 12),
),
trailing: const Icon(
Icons.open_in_new,
color: Colors.white38,
size: 18,
), ),
title: const Text('Waze', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
subtitle: const Text('Open road directions in Waze', style: TextStyle(color: Colors.white54, fontSize: 12)),
trailing: const Icon(Icons.open_in_new, color: Colors.white38, size: 18),
onTap: () async { onTap: () async {
Navigator.pop(sheetContext); Navigator.pop(sheetContext);
final uri = buildWazeUri( final uri = buildWazeUri(
@ -155,12 +197,16 @@ class NavigationHandoffService {
longitude: longitude, longitude: longitude,
); );
if (await canLaunchUrl(uri)) { if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication); await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
} }
}, },
), ),
const Divider(color: Colors.white12, height: 1), const Divider(color: Colors.white12, height: 1),
ListTile( ListTile(
key: const ValueKey('handoff_system_map_tile'),
leading: Container( leading: Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -169,9 +215,22 @@ class NavigationHandoffService {
), ),
child: const Icon(Icons.place, color: Colors.white70), child: const Icon(Icons.place, color: Colors.white70),
), ),
title: const Text('System Map', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)), title: const Text(
subtitle: const Text('Open in default platform mapping app', style: TextStyle(color: Colors.white54, fontSize: 12)), 'System Map',
trailing: const Icon(Icons.open_in_new, color: Colors.white38, size: 18), style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
subtitle: const Text(
'Open in default platform mapping app',
style: TextStyle(color: Colors.white54, fontSize: 12),
),
trailing: const Icon(
Icons.open_in_new,
color: Colors.white38,
size: 18,
),
onTap: () async { onTap: () async {
Navigator.pop(sheetContext); Navigator.pop(sheetContext);
final uri = buildSystemGeoUri( final uri = buildSystemGeoUri(
@ -180,7 +239,10 @@ class NavigationHandoffService {
displayName: displayName, displayName: displayName,
); );
if (await canLaunchUrl(uri)) { if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication); await launchUrl(
uri,
mode: LaunchMode.externalApplication,
);
} }
}, },
), ),
@ -209,10 +271,7 @@ class NavigationHandoffService {
break; break;
case NavigationApp.waze: case NavigationApp.waze:
uri = buildWazeUri( uri = buildWazeUri(latitude: stop.latitude, longitude: stop.longitude);
latitude: stop.latitude,
longitude: stop.longitude,
);
break; break;
case NavigationApp.platformDefault: case NavigationApp.platformDefault:
@ -232,7 +291,10 @@ class NavigationHandoffService {
'https://www.google.com/maps/search/?api=1&query=${stop.latitude},${stop.longitude}', 'https://www.google.com/maps/search/?api=1&query=${stop.latitude},${stop.longitude}',
); );
if (await canLaunchUrl(fallbackUri)) { if (await canLaunchUrl(fallbackUri)) {
return await launchUrl(fallbackUri, mode: LaunchMode.externalApplication); return await launchUrl(
fallbackUri,
mode: LaunchMode.externalApplication,
);
} }
return false; return false;
} catch (e) { } catch (e) {
@ -262,8 +324,9 @@ class NavigationHandoffService {
.map((s) => '${s.latitude},${s.longitude}') .map((s) => '${s.latitude},${s.longitude}')
.join('|'); .join('|');
final waypointsParam = final waypointsParam = waypointsStr.isNotEmpty
waypointsStr.isNotEmpty ? '&waypoints=$waypointsStr' : ''; ? '&waypoints=$waypointsStr'
: '';
final uri = Uri.parse( final uri = Uri.parse(
'https://www.google.com/maps/dir/?api=1&origin=$origin&destination=$dest$waypointsParam', 'https://www.google.com/maps/dir/?api=1&origin=$origin&destination=$dest$waypointsParam',
); );
@ -273,7 +336,9 @@ class NavigationHandoffService {
return await launchUrl(uri, mode: LaunchMode.externalApplication); return await launchUrl(uri, mode: LaunchMode.externalApplication);
} }
} catch (e) { } catch (e) {
debugPrint('[NavigationHandoffService] Failed to launch multi-stop route: $e'); debugPrint(
'[NavigationHandoffService] Failed to launch multi-stop route: $e',
);
} }
} }

View File

@ -925,6 +925,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
marionette_flutter:
dependency: "direct main"
description:
name: marionette_flutter
sha256: "9bf6aa531157edeb3d94f7c4972367240a1d704bbe9f4ac558ef1a125c4d1a6d"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -65,6 +65,7 @@ dependencies:
cloud_firestore: ^5.6.5 cloud_firestore: ^5.6.5
flutter_secure_storage: ^10.2.1 flutter_secure_storage: ^10.2.1
in_app_purchase: 3.3.0 in_app_purchase: 3.3.0
marionette_flutter: ^0.6.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: