diff --git a/lib/core/widgets/main_layout.dart b/lib/core/widgets/main_layout.dart index 06c2183..2304127 100644 --- a/lib/core/widgets/main_layout.dart +++ b/lib/core/widgets/main_layout.dart @@ -33,13 +33,15 @@ class _MainLayoutState extends ConsumerState { @override Widget build(BuildContext context) { ref.listen(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) { widget.navigationShell.goBranch(0); } } }); - final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape; + final isLandscape = + MediaQuery.of(context).orientation == Orientation.landscape; if (isLandscape) { return Scaffold( @@ -52,7 +54,9 @@ class _MainLayoutState extends ConsumerState { return SingleChildScrollView( physics: const ClampingScrollPhysics(), child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), child: IntrinsicHeight( child: NavigationRail( minWidth: 54, @@ -61,13 +65,22 @@ class _MainLayoutState extends ConsumerState { onDestinationSelected: (index) { widget.navigationShell.goBranch( index, - initialLocation: index == widget.navigationShell.currentIndex, + initialLocation: + index == widget.navigationShell.currentIndex, ); }, backgroundColor: const Color(0xFF0A0A0A), - indicatorColor: const Color(0xFF00E5FF).withValues(alpha: 0.18), - selectedIconTheme: const IconThemeData(color: Color(0xFF00E5FF), size: 20), - unselectedIconTheme: const IconThemeData(color: Colors.white54, size: 18), + indicatorColor: const Color( + 0xFF00E5FF, + ).withValues(alpha: 0.18), + selectedIconTheme: const IconThemeData( + color: Color(0xFF00E5FF), + size: 20, + ), + unselectedIconTheme: const IconThemeData( + color: Colors.white54, + size: 18, + ), selectedLabelTextStyle: const TextStyle( fontFamily: 'Inter', color: Color(0xFF00E5FF), @@ -84,32 +97,50 @@ class _MainLayoutState extends ConsumerState { destinations: const [ NavigationRailDestination( padding: EdgeInsets.symmetric(vertical: 2), - icon: Icon(Icons.speed), + icon: Icon( + Icons.speed, + key: ValueKey('rail_dash_icon'), + ), label: Text('Dash'), ), NavigationRailDestination( padding: EdgeInsets.symmetric(vertical: 2), - icon: Icon(Icons.map_outlined), + icon: Icon( + Icons.map_outlined, + key: ValueKey('rail_map_icon'), + ), label: Text('Map'), ), NavigationRailDestination( padding: EdgeInsets.symmetric(vertical: 2), - icon: Icon(Icons.history), + icon: Icon( + Icons.history, + key: ValueKey('rail_history_icon'), + ), label: Text('History'), ), NavigationRailDestination( 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'), ), NavigationRailDestination( padding: EdgeInsets.symmetric(vertical: 2), - icon: Icon(Icons.two_wheeler), + icon: Icon( + Icons.two_wheeler, + key: ValueKey('rail_garage_icon'), + ), label: Text('Garage'), ), NavigationRailDestination( padding: EdgeInsets.symmetric(vertical: 2), - icon: Icon(Icons.settings_outlined), + icon: Icon( + Icons.settings_outlined, + key: ValueKey('rail_settings_icon'), + ), label: Text('Settings'), ), ], @@ -120,7 +151,11 @@ class _MainLayoutState extends ConsumerState { }, ), ), - const VerticalDivider(width: 1, thickness: 1, color: Color(0xFF1F1F1F)), + const VerticalDivider( + width: 1, + thickness: 1, + color: Color(0xFF1F1F1F), + ), Expanded(child: widget.navigationShell), ], ), @@ -130,6 +165,7 @@ class _MainLayoutState extends ConsumerState { return Scaffold( body: widget.navigationShell, bottomNavigationBar: BottomNavigationBar( + key: const ValueKey('main_bottom_nav_bar'), currentIndex: widget.navigationShell.currentIndex, onTap: (index) { widget.navigationShell.goBranch( @@ -143,28 +179,34 @@ class _MainLayoutState extends ConsumerState { type: BottomNavigationBarType.fixed, items: const [ BottomNavigationBarItem( - icon: Icon(Icons.speed), + icon: Icon(Icons.speed, key: ValueKey('nav_dash_icon')), label: 'Dash', + tooltip: 'Dash', ), BottomNavigationBarItem( - icon: Icon(Icons.map), + icon: Icon(Icons.map, key: ValueKey('nav_map_icon')), label: 'Map', + tooltip: 'Map', ), BottomNavigationBarItem( - icon: Icon(Icons.history), + icon: Icon(Icons.history, key: ValueKey('nav_history_icon')), label: 'History', + tooltip: 'History', ), BottomNavigationBarItem( - icon: Icon(Icons.emoji_events), + icon: Icon(Icons.emoji_events, key: ValueKey('nav_records_icon')), label: 'Records', + tooltip: 'Records', ), BottomNavigationBarItem( - icon: Icon(Icons.two_wheeler), + icon: Icon(Icons.two_wheeler, key: ValueKey('nav_garage_icon')), label: 'Garage', + tooltip: 'Garage', ), BottomNavigationBarItem( - icon: Icon(Icons.settings), + icon: Icon(Icons.settings, key: ValueKey('nav_settings_icon')), label: 'Settings', + tooltip: 'Settings', ), ], ), diff --git a/lib/features/garage/garage_screen.dart b/lib/features/garage/garage_screen.dart index 985bb00..1c844b7 100644 --- a/lib/features/garage/garage_screen.dart +++ b/lib/features/garage/garage_screen.dart @@ -39,6 +39,7 @@ class GarageScreen extends ConsumerWidget { ), actions: [ IconButton( + key: const ValueKey('garage_add_motorcycle_appbar_button'), icon: const Icon( Icons.add_circle_outline, color: Color(0xFF00E5FF), @@ -71,6 +72,7 @@ class GarageScreen extends ConsumerWidget { floatingActionButton: bikesAsync.value != null && bikesAsync.value!.isNotEmpty ? FloatingActionButton.extended( + key: const ValueKey('garage_add_motorcycle_fab'), backgroundColor: Colors.cyanAccent, foregroundColor: Colors.black, icon: const Icon(Icons.two_wheeler), @@ -130,6 +132,7 @@ class GarageScreen extends ConsumerWidget { ), const SizedBox(height: 32), ElevatedButton.icon( + key: const ValueKey('garage_add_motorcycle_empty_button'), style: ElevatedButton.styleFrom( backgroundColor: Colors.cyanAccent, foregroundColor: Colors.black, diff --git a/lib/features/group_rides/group_ride_detail_screen.dart b/lib/features/group_rides/group_ride_detail_screen.dart index 73dd92e..0c00f88 100644 --- a/lib/features/group_rides/group_ride_detail_screen.dart +++ b/lib/features/group_rides/group_ride_detail_screen.dart @@ -24,17 +24,19 @@ class _GroupRideDetailScreenState extends ConsumerState { Future _launchMapNavigation(GroupRideLocation loc) async { final query = Uri.encodeComponent( - loc.address.isNotEmpty - ? loc.address - : '${loc.latitude},${loc.longitude}', + loc.address.isNotEmpty ? loc.address : '${loc.latitude},${loc.longitude}', + ); + final uri = Uri.parse( + 'https://www.google.com/maps/search/?api=1&query=$query', ); - final uri = Uri.parse('https://www.google.com/maps/search/?api=1&query=$query'); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Could not open map navigation application.')), + const SnackBar( + content: Text('Could not open map navigation application.'), + ), ); } } @@ -82,14 +84,20 @@ class _GroupRideDetailScreenState extends ConsumerState { ListTile( leading: CircleAvatar( backgroundColor: Colors.white10, - child: Text(f.displayName.isNotEmpty - ? f.displayName[0].toUpperCase() - : '?'), + child: Text( + f.displayName.isNotEmpty + ? f.displayName[0].toUpperCase() + : '?', + ), + ), + title: Text( + f.displayName, + style: const TextStyle(color: Colors.white), + ), + subtitle: Text( + f.displayHandle, + style: const TextStyle(color: Colors.white54), ), - title: Text(f.displayName, - style: const TextStyle(color: Colors.white)), - subtitle: Text(f.displayHandle, - style: const TextStyle(color: Colors.white54)), trailing: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF00E5FF), @@ -97,8 +105,9 @@ class _GroupRideDetailScreenState extends ConsumerState { ), onPressed: () async { try { - final currentProfile = - ref.read(myRiderProfileProvider).value; + final currentProfile = ref + .read(myRiderProfileProvider) + .value; if (currentProfile == null) return; final targetProfile = await ref @@ -144,8 +153,9 @@ class _GroupRideDetailScreenState extends ConsumerState { @override Widget build(BuildContext context) { final rideAsync = ref.watch(groupRideStreamProvider(widget.rideId)); - final participantsAsync = - ref.watch(groupRideParticipantsProvider(widget.rideId)); + final participantsAsync = ref.watch( + groupRideParticipantsProvider(widget.rideId), + ); final user = ref.watch(currentUserProvider); final currentUid = user?.uid; @@ -166,19 +176,23 @@ class _GroupRideDetailScreenState extends ConsumerState { data: (ride) { if (ride == null) { return const Center( - child: Text('Group ride not found.', - style: TextStyle(color: Colors.white70)), + child: Text( + 'Group ride not found.', + style: TextStyle(color: Colors.white70), + ), ); } - final isOrganizer = currentUid != null && ride.organizerUid == currentUid; + final isOrganizer = + currentUid != null && ride.organizerUid == currentUid; return participantsAsync.when( data: (participants) { GroupRideParticipant? myParticipant; try { - myParticipant = - participants.firstWhere((p) => p.userId == currentUid); + myParticipant = participants.firstWhere( + (p) => p.userId == currentUid, + ); } catch (_) {} return ListView( @@ -193,9 +207,13 @@ class _GroupRideDetailScreenState extends ConsumerState { borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.cyanAccent), ), - child: Text(_statusMessage!, - style: const TextStyle( - color: Colors.cyanAccent, fontSize: 13)), + child: Text( + _statusMessage!, + style: const TextStyle( + color: Colors.cyanAccent, + fontSize: 13, + ), + ), ), // Header card @@ -261,23 +279,34 @@ class _GroupRideDetailScreenState extends ConsumerState { ); }, loading: () => const Center( - child: CircularProgressIndicator(color: Color(0xFF00E5FF))), + child: CircularProgressIndicator(color: Color(0xFF00E5FF)), + ), error: (e, _) => Center( - child: Text('Error loading participants: $e', - style: const TextStyle(color: Colors.redAccent))), + child: Text( + 'Error loading participants: $e', + style: const TextStyle(color: Colors.redAccent), + ), + ), ); }, loading: () => const Center( - child: CircularProgressIndicator(color: Color(0xFF00E5FF))), + child: CircularProgressIndicator(color: Color(0xFF00E5FF)), + ), error: (e, _) => Center( - child: Text('Error loading ride: $e', - style: const TextStyle(color: Colors.redAccent))), + child: Text( + 'Error loading ride: $e', + style: const TextStyle(color: Colors.redAccent), + ), + ), ), ); } Widget _buildHeaderCard( - BuildContext context, GroupRide ride, bool isOrganizer) { + BuildContext context, + GroupRide ride, + bool isOrganizer, + ) { return Card( color: const Color(0xFF161922), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), @@ -289,8 +318,10 @@ class _GroupRideDetailScreenState extends ConsumerState { Row( children: [ Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), decoration: BoxDecoration( color: ride.isCanceled ? Colors.redAccent.withValues(alpha: 0.2) @@ -310,8 +341,10 @@ class _GroupRideDetailScreenState extends ConsumerState { ), const SizedBox(width: 8), Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.circular(6), @@ -340,13 +373,17 @@ class _GroupRideDetailScreenState extends ConsumerState { itemBuilder: (ctx) => [ const PopupMenuItem( value: 'invite', - child: Text('Invite Friends', - style: TextStyle(color: Colors.white)), + child: Text( + 'Invite Friends', + style: TextStyle(color: Colors.white), + ), ), const PopupMenuItem( value: 'cancel', - child: Text('Cancel Ride', - style: TextStyle(color: Colors.redAccent)), + child: Text( + 'Cancel Ride', + style: TextStyle(color: Colors.redAccent), + ), ), ], ), @@ -371,7 +408,10 @@ class _GroupRideDetailScreenState extends ConsumerState { Text( ride.description, style: const TextStyle( - color: Colors.white70, fontSize: 13, height: 1.4), + color: Colors.white70, + fontSize: 13, + height: 1.4, + ), ), ], if (ride.canceledReason != null) ...[ @@ -382,18 +422,24 @@ class _GroupRideDetailScreenState extends ConsumerState { color: Colors.redAccent.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all( - color: Colors.redAccent.withValues(alpha: 0.4)), + color: Colors.redAccent.withValues(alpha: 0.4), + ), ), child: Row( children: [ - const Icon(Icons.info_outline, - color: Colors.redAccent, size: 16), + const Icon( + Icons.info_outline, + color: Colors.redAccent, + size: 16, + ), const SizedBox(width: 8), Expanded( child: Text( 'Canceled: ${ride.canceledReason}', style: const TextStyle( - color: Colors.redAccent, fontSize: 12), + color: Colors.redAccent, + fontSize: 12, + ), ), ), ], @@ -561,8 +607,10 @@ class _GroupRideDetailScreenState extends ConsumerState { ), TextButton( onPressed: () => _confirmLeaveRide(ride, currentUid), - child: const Text('LEAVE RIDE', - style: TextStyle(color: Colors.redAccent)), + child: const Text( + 'LEAVE RIDE', + style: TextStyle(color: Colors.redAccent), + ), ), ], ); @@ -576,12 +624,15 @@ class _GroupRideDetailScreenState extends ConsumerState { backgroundColor: const Color(0xFF00E5FF), foregroundColor: Colors.black, minimumSize: const Size.fromHeight(46), - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), icon: const Icon(Icons.group_add), - label: const Text('JOIN GROUP RIDE', - style: TextStyle(fontWeight: FontWeight.bold)), + label: const Text( + 'JOIN GROUP RIDE', + style: TextStyle(fontWeight: FontWeight.bold), + ), onPressed: _isProcessing ? null : () async { @@ -608,8 +659,9 @@ class _GroupRideDetailScreenState extends ConsumerState { foregroundColor: const Color(0xFF00E5FF), side: const BorderSide(color: Color(0xFF00E5FF)), minimumSize: const Size.fromHeight(46), - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), icon: const Icon(Icons.send), label: const Text('REQUEST TO JOIN'), @@ -670,8 +722,10 @@ class _GroupRideDetailScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('Meetup Arrival', - style: TextStyle(color: Colors.white60, fontSize: 12)), + const Text( + 'Meetup Arrival', + style: TextStyle(color: Colors.white60, fontSize: 12), + ), const SizedBox(height: 4), Text( _formatTime(ride.scheduledStartAt), @@ -683,7 +737,10 @@ class _GroupRideDetailScreenState extends ConsumerState { ), Text( '${ride.scheduledStartAt.month}/${ride.scheduledStartAt.day}/${ride.scheduledStartAt.year}', - style: const TextStyle(color: Colors.white54, fontSize: 11), + style: const TextStyle( + color: Colors.white54, + fontSize: 11, + ), ), ], ), @@ -693,8 +750,10 @@ class _GroupRideDetailScreenState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - const Text('Kickstands Up (KSU)', - style: TextStyle(color: Colors.white60, fontSize: 12)), + const Text( + 'Kickstands Up (KSU)', + style: TextStyle(color: Colors.white60, fontSize: 12), + ), const SizedBox(height: 4), Text( _formatTime(ride.departureAt), @@ -706,7 +765,10 @@ class _GroupRideDetailScreenState extends ConsumerState { ), Text( 'Departure', - style: const TextStyle(color: Colors.white54, fontSize: 11), + style: const TextStyle( + color: Colors.white54, + fontSize: 11, + ), ), ], ), @@ -769,24 +831,29 @@ class _GroupRideDetailScreenState extends ConsumerState { Text( ride.meetingPoint.address, style: const TextStyle( - color: Colors.white70, fontSize: 12), + color: Colors.white70, + fontSize: 12, + ), ), if (ride.meetingPoint.notes.isNotEmpty) ...[ const SizedBox(height: 4), Text( 'Note: ${ride.meetingPoint.notes}', style: const TextStyle( - color: Colors.white54, - fontSize: 11, - fontStyle: FontStyle.italic), + color: Colors.white54, + fontSize: 11, + fontStyle: FontStyle.italic, + ), ), ], ], ), ), IconButton( - icon: const Icon(Icons.navigation_outlined, - color: Color(0xFF00E5FF)), + icon: const Icon( + Icons.navigation_outlined, + color: Color(0xFF00E5FF), + ), tooltip: 'Open in Map App', onPressed: () => _launchMapNavigation(ride.meetingPoint), ), @@ -822,14 +889,18 @@ class _GroupRideDetailScreenState extends ConsumerState { Text( ride.destination!.address, style: const TextStyle( - color: Colors.white70, fontSize: 12), + color: Colors.white70, + fontSize: 12, + ), ), ], ), ), IconButton( - icon: const Icon(Icons.navigation_outlined, - color: Color(0xFFFFB300)), + icon: const Icon( + Icons.navigation_outlined, + color: Color(0xFFFFB300), + ), tooltip: 'Open in Map App', onPressed: () => _launchMapNavigation(ride.destination!), ), @@ -878,7 +949,10 @@ class _GroupRideDetailScreenState extends ConsumerState { ), if (hasRoute) Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), decoration: BoxDecoration( color: const Color(0xFF00E5FF).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), @@ -917,20 +991,31 @@ class _GroupRideDetailScreenState extends ConsumerState { runSpacing: 8, children: [ ElevatedButton.icon( + key: const ValueKey('view_attached_route_button'), onPressed: () => context.push('/routes/${ride.routeId}'), icon: const Icon(Icons.map, size: 16), label: const Text('VIEW ROUTE / PLANNER'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF00E5FF), foregroundColor: Colors.black, - textStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold), + textStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), ), ), if (isOrganizer && !ride.isCanceled && !ride.isCompleted) ...[ OutlinedButton.icon( onPressed: () => _showAttachRouteSheet(context, ride), - icon: const Icon(Icons.swap_horiz, size: 16, color: Color(0xFF00E5FF)), - label: const Text('CHANGE', style: TextStyle(color: Color(0xFF00E5FF))), + icon: const Icon( + Icons.swap_horiz, + size: 16, + color: Color(0xFF00E5FF), + ), + label: const Text( + 'CHANGE', + style: TextStyle(color: Color(0xFF00E5FF)), + ), style: OutlinedButton.styleFrom( side: const BorderSide(color: Color(0xFF2A2E3D)), textStyle: const TextStyle(fontSize: 12), @@ -939,22 +1024,37 @@ class _GroupRideDetailScreenState extends ConsumerState { OutlinedButton.icon( onPressed: () async { try { - await ref.read(routeServiceProvider).detachRouteFromGroupRide(ride.id); + await ref + .read(routeServiceProvider) + .detachRouteFromGroupRide(ride.id); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Route detached from group ride.')), + const SnackBar( + content: Text( + 'Route detached from group ride.', + ), + ), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to detach route: $e')), + SnackBar( + content: Text('Failed to detach route: $e'), + ), ); } } }, - icon: const Icon(Icons.link_off, size: 16, color: Color(0xFFFF5252)), - label: const Text('DETACH', style: TextStyle(color: Color(0xFFFF5252))), + icon: const Icon( + Icons.link_off, + size: 16, + color: Color(0xFFFF5252), + ), + label: const Text( + 'DETACH', + style: TextStyle(color: Color(0xFFFF5252)), + ), style: OutlinedButton.styleFrom( side: const BorderSide(color: Color(0xFFFF5252)), textStyle: const TextStyle(fontSize: 12), @@ -980,19 +1080,33 @@ class _GroupRideDetailScreenState extends ConsumerState { runSpacing: 8, children: [ ElevatedButton.icon( - onPressed: () => context.push('/routes/planner?groupRideId=${ride.id}'), + key: const ValueKey('plan_new_route_button'), + onPressed: () => context.push( + '/routes/planner?groupRideId=${ride.id}', + ), icon: const Icon(Icons.add_road, size: 16), label: const Text('PLAN NEW ROUTE'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF00E5FF), foregroundColor: Colors.black, - textStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold), + textStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), ), ), OutlinedButton.icon( + key: const ValueKey('attach_saved_route_button'), onPressed: () => _showAttachRouteSheet(context, ride), - icon: const Icon(Icons.link, size: 16, color: Color(0xFF00E5FF)), - label: const Text('ATTACH SAVED ROUTE', style: TextStyle(color: Color(0xFF00E5FF))), + icon: const Icon( + Icons.link, + size: 16, + color: Color(0xFF00E5FF), + ), + label: const Text( + 'ATTACH SAVED ROUTE', + style: TextStyle(color: Color(0xFF00E5FF)), + ), style: OutlinedButton.styleFrom( side: const BorderSide(color: Color(0xFF2A2E3D)), textStyle: const TextStyle(fontSize: 12), @@ -1042,7 +1156,9 @@ class _GroupRideDetailScreenState extends ConsumerState { loading: () => const Center( child: Padding( padding: EdgeInsets.all(24), - child: CircularProgressIndicator(color: Color(0xFF00E5FF)), + child: CircularProgressIndicator( + color: Color(0xFF00E5FF), + ), ), ), error: (err, _) => Text( @@ -1064,7 +1180,9 @@ class _GroupRideDetailScreenState extends ConsumerState { ElevatedButton( onPressed: () { Navigator.pop(sheetCtx); - context.push('/routes/planner?groupRideId=${ride.id}'); + context.push( + '/routes/planner?groupRideId=${ride.id}', + ); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF00E5FF), @@ -1087,35 +1205,61 @@ class _GroupRideDetailScreenState extends ConsumerState { itemBuilder: (ctx, idx) { final route = routes[idx]; return ListTile( - leading: const Icon(Icons.alt_route, color: Color(0xFF00E5FF)), + leading: const Icon( + Icons.alt_route, + color: Color(0xFF00E5FF), + ), title: Text( route.title, - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), subtitle: Text( '${route.stops.length} stops • ${route.formattedDistanceMiles}', - style: const TextStyle(color: Colors.white54, fontSize: 12), + style: const TextStyle( + color: Colors.white54, + fontSize: 12, + ), + ), + trailing: const Icon( + Icons.chevron_right, + color: Colors.white30, ), - trailing: const Icon(Icons.chevron_right, color: Colors.white30), onTap: () async { Navigator.pop(sheetCtx); try { - await ref.read(routeServiceProvider).attachRouteToGroupRide( - groupRideId: ride.id, - route: route, - ); + await ref + .read(routeServiceProvider) + .attachRouteToGroupRide( + groupRideId: ride.id, + route: route, + ); if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( + ScaffoldMessenger.of( + context, + ).showSnackBar( SnackBar( - content: Text('Attached "${route.title}" to group ride.'), - backgroundColor: const Color(0xFF00E676), + content: Text( + 'Attached "${route.title}" to group ride.', + ), + backgroundColor: const Color( + 0xFF00E676, + ), ), ); } } catch (e) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to attach route: $e')), + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + 'Failed to attach route: $e', + ), + ), ); } } @@ -1172,9 +1316,10 @@ class _GroupRideDetailScreenState extends ConsumerState { child: Text( '${i + 1}', style: const TextStyle( - fontSize: 10, - color: Colors.white, - fontWeight: FontWeight.bold), + fontSize: 10, + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), ), const SizedBox(width: 10), @@ -1185,20 +1330,27 @@ class _GroupRideDetailScreenState extends ConsumerState { Text( ride.stops[i].displayName, style: const TextStyle( - color: Colors.white, fontSize: 13), + color: Colors.white, + fontSize: 13, + ), ), Text( ride.stops[i].locationType.label, style: const TextStyle( - color: Colors.white54, fontSize: 11), + color: Colors.white54, + fontSize: 11, + ), ), ], ), ), if (ride.stops[i].address.isNotEmpty) IconButton( - icon: const Icon(Icons.navigation_outlined, - size: 16, color: Colors.white60), + icon: const Icon( + Icons.navigation_outlined, + size: 16, + color: Colors.white60, + ), onPressed: () => _launchMapNavigation(ride.stops[i]), ), ], @@ -1216,12 +1368,15 @@ class _GroupRideDetailScreenState extends ConsumerState { bool isOrganizer, String? currentUid, ) { - final active = - participants.where((p) => p.status == ParticipantStatus.accepted).toList(); - final pendingRequests = - participants.where((p) => p.status == ParticipantStatus.requested).toList(); - final invited = - participants.where((p) => p.status == ParticipantStatus.invited).toList(); + final active = participants + .where((p) => p.status == ParticipantStatus.accepted) + .toList(); + final pendingRequests = participants + .where((p) => p.status == ParticipantStatus.requested) + .toList(); + final invited = participants + .where((p) => p.status == ParticipantStatus.invited) + .toList(); final service = ref.read(groupRideServiceProvider); @@ -1238,8 +1393,11 @@ class _GroupRideDetailScreenState extends ConsumerState { children: [ Row( children: [ - const Icon(Icons.people_alt_outlined, - color: Color(0xFF00E5FF), size: 18), + const Icon( + Icons.people_alt_outlined, + color: Color(0xFF00E5FF), + size: 18, + ), const SizedBox(width: 8), Text( 'PARTICIPANTS (${active.length}${ride.participantLimit != null ? '/${ride.participantLimit}' : ''})', @@ -1267,18 +1425,23 @@ class _GroupRideDetailScreenState extends ConsumerState { const Text( 'JOIN REQUESTS', style: TextStyle( - color: Colors.amber, - fontWeight: FontWeight.bold, - fontSize: 11), + color: Colors.amber, + fontWeight: FontWeight.bold, + fontSize: 11, + ), ), const SizedBox(height: 6), for (final req in pendingRequests) ListTile( contentPadding: EdgeInsets.zero, - title: Text(req.riderDisplayName, - style: const TextStyle(color: Colors.white, fontSize: 13)), - subtitle: Text(req.displayHandle, - style: const TextStyle(color: Colors.white54, fontSize: 11)), + title: Text( + req.riderDisplayName, + style: const TextStyle(color: Colors.white, fontSize: 13), + ), + subtitle: Text( + req.displayHandle, + style: const TextStyle(color: Colors.white54, fontSize: 11), + ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -1330,16 +1493,22 @@ class _GroupRideDetailScreenState extends ConsumerState { ), ), ), - title: Text(p.riderDisplayName, - style: const TextStyle(color: Colors.white, fontSize: 13)), + title: Text( + p.riderDisplayName, + style: const TextStyle(color: Colors.white, fontSize: 13), + ), subtitle: Text( '${p.displayHandle} • ${p.role.label}', style: const TextStyle(color: Colors.white54, fontSize: 11), ), - trailing: isOrganizer && p.userId != currentUid && !ride.isCanceled + trailing: + isOrganizer && p.userId != currentUid && !ride.isCanceled ? PopupMenuButton( - icon: const Icon(Icons.more_vert, - size: 18, color: Colors.white38), + icon: const Icon( + Icons.more_vert, + size: 18, + color: Colors.white38, + ), color: const Color(0xFF1B1E28), onSelected: (action) async { if (action == 'remove') { @@ -1359,13 +1528,17 @@ class _GroupRideDetailScreenState extends ConsumerState { itemBuilder: (ctx) => [ const PopupMenuItem( value: 'transfer', - child: Text('Make Organizer', - style: TextStyle(color: Colors.white)), + child: Text( + 'Make Organizer', + style: TextStyle(color: Colors.white), + ), ), const PopupMenuItem( value: 'remove', - child: Text('Remove Rider', - style: TextStyle(color: Colors.redAccent)), + child: Text( + 'Remove Rider', + style: TextStyle(color: Colors.redAccent), + ), ), ], ) @@ -1378,18 +1551,23 @@ class _GroupRideDetailScreenState extends ConsumerState { const Text( 'INVITATIONS PENDING', style: TextStyle( - color: Colors.white60, - fontWeight: FontWeight.bold, - fontSize: 11), + color: Colors.white60, + fontWeight: FontWeight.bold, + fontSize: 11, + ), ), const SizedBox(height: 6), for (final inv in invited) ListTile( contentPadding: EdgeInsets.zero, - title: Text(inv.riderDisplayName, - style: const TextStyle(color: Colors.white70, fontSize: 12)), - subtitle: Text('${inv.displayHandle} • Invited', - style: const TextStyle(color: Colors.white38, fontSize: 10)), + title: Text( + inv.riderDisplayName, + style: const TextStyle(color: Colors.white70, fontSize: 12), + ), + subtitle: Text( + '${inv.displayHandle} • Invited', + style: const TextStyle(color: Colors.white38, fontSize: 10), + ), ), ], ], @@ -1403,8 +1581,10 @@ class _GroupRideDetailScreenState extends ConsumerState { context: context, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF1B1E28), - title: const Text('Cancel Group Ride?', - style: TextStyle(color: Colors.white)), + title: const Text( + 'Cancel Group Ride?', + style: TextStyle(color: Colors.white), + ), content: const Text( 'This will cancel the group ride for all participants.', style: TextStyle(color: Colors.white70, fontSize: 13), @@ -1412,8 +1592,10 @@ class _GroupRideDetailScreenState extends ConsumerState { actions: [ TextButton( onPressed: () => Navigator.pop(ctx), - child: - const Text('KEEP RIDE', style: TextStyle(color: Colors.white60)), + child: const Text( + 'KEEP RIDE', + style: TextStyle(color: Colors.white60), + ), ), ElevatedButton( style: ElevatedButton.styleFrom( @@ -1424,7 +1606,9 @@ class _GroupRideDetailScreenState extends ConsumerState { Navigator.pop(ctx); final user = ref.read(currentUserProvider); if (user == null) return; - await ref.read(groupRideServiceProvider).cancelGroupRide( + await ref + .read(groupRideServiceProvider) + .cancelGroupRide( callerUid: user.uid, groupRideId: ride.id, reason: 'Canceled by organizer.', @@ -1442,8 +1626,10 @@ class _GroupRideDetailScreenState extends ConsumerState { context: context, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF1B1E28), - title: - const Text('Leave Group Ride?', style: TextStyle(color: Colors.white)), + title: const Text( + 'Leave Group Ride?', + style: TextStyle(color: Colors.white), + ), content: const Text( 'You will no longer be listed as a participant.', style: TextStyle(color: Colors.white70, fontSize: 13), @@ -1460,10 +1646,9 @@ class _GroupRideDetailScreenState extends ConsumerState { ), onPressed: () async { Navigator.pop(ctx); - await ref.read(groupRideServiceProvider).leaveRide( - callerUid: currentUid, - groupRideId: ride.id, - ); + await ref + .read(groupRideServiceProvider) + .leaveRide(callerUid: currentUid, groupRideId: ride.id); }, child: const Text('LEAVE'), ), @@ -1496,7 +1681,11 @@ class _GroupRideDetailScreenState extends ConsumerState { children: [ const Row( children: [ - Icon(Icons.admin_panel_settings, color: Color(0xFF00E5FF), size: 18), + Icon( + Icons.admin_panel_settings, + color: Color(0xFF00E5FF), + size: 18, + ), SizedBox(width: 8), Text( 'ORGANIZER CONTROLS', @@ -1522,7 +1711,9 @@ class _GroupRideDetailScreenState extends ConsumerState { if (ride.status == GroupRideStatus.scheduled) ...[ ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.2), + backgroundColor: const Color( + 0xFF00E5FF, + ).withValues(alpha: 0.2), foregroundColor: const Color(0xFF00E5FF), side: const BorderSide(color: Color(0xFF00E5FF)), ), @@ -1546,7 +1737,10 @@ class _GroupRideDetailScreenState extends ConsumerState { foregroundColor: Colors.black, ), icon: const Icon(Icons.two_wheeler, size: 16), - label: const Text('START RIDE (UNDERWAY)', style: TextStyle(fontWeight: FontWeight.bold)), + label: const Text( + 'START RIDE (UNDERWAY)', + style: TextStyle(fontWeight: FontWeight.bold), + ), onPressed: _isProcessing || currentUid == null ? null : () async { @@ -1567,7 +1761,10 @@ class _GroupRideDetailScreenState extends ConsumerState { foregroundColor: Colors.black, ), icon: const Icon(Icons.two_wheeler, size: 16), - label: const Text('START RIDE (UNDERWAY)', style: TextStyle(fontWeight: FontWeight.bold)), + label: const Text( + 'START RIDE (UNDERWAY)', + style: TextStyle(fontWeight: FontWeight.bold), + ), onPressed: _isProcessing || currentUid == null ? null : () async { @@ -1584,12 +1781,17 @@ class _GroupRideDetailScreenState extends ConsumerState { if (ride.status == GroupRideStatus.underway) ...[ ElevatedButton.icon( style: ElevatedButton.styleFrom( - backgroundColor: Colors.greenAccent.withValues(alpha: 0.2), + backgroundColor: Colors.greenAccent.withValues( + alpha: 0.2, + ), foregroundColor: Colors.greenAccent, side: const BorderSide(color: Colors.greenAccent), ), icon: const Icon(Icons.flag, size: 16), - label: const Text('FINISH / COMPLETE RIDE', style: TextStyle(fontWeight: FontWeight.bold)), + label: const Text( + 'FINISH / COMPLETE RIDE', + style: TextStyle(fontWeight: FontWeight.bold), + ), onPressed: _isProcessing || currentUid == null ? null : () async { @@ -1630,9 +1832,7 @@ class _GroupRideDetailScreenState extends ConsumerState { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide( - color: isCurrentlySharing - ? const Color(0xFF00E5FF) - : Colors.white12, + color: isCurrentlySharing ? const Color(0xFF00E5FF) : Colors.white12, width: isCurrentlySharing ? 1.2 : 0.8, ), ), @@ -1734,9 +1934,20 @@ class _GroupRideDetailScreenState extends ConsumerState { minimumSize: Size.zero, ), onPressed: () { - ref.read(groupRideLiveSharingAllowedProvider.notifier).state = true; + ref + .read( + groupRideLiveSharingAllowedProvider.notifier, + ) + .state = + true; }, - child: const Text('ALLOW LOCATION SHARING', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold)), + child: const Text( + 'ALLOW LOCATION SHARING', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), ), ], ), @@ -1767,7 +1978,8 @@ class _GroupRideDetailScreenState extends ConsumerState { activeThumbColor: const Color(0xFF00E5FF), onChanged: (val) async { if (val) { - ref.read(activeLiveRideSharingIdProvider.notifier).state = ride.id; + ref.read(activeLiveRideSharingIdProvider.notifier).state = + ride.id; await service.publishLivePresence( callerUid: currentUid, groupRideId: ride.id, @@ -1777,7 +1989,8 @@ class _GroupRideDetailScreenState extends ConsumerState { sharingEnabled: true, ); } else { - ref.read(activeLiveRideSharingIdProvider.notifier).state = null; + ref.read(activeLiveRideSharingIdProvider.notifier).state = + null; await service.clearLivePresence( callerUid: currentUid, groupRideId: ride.id, @@ -1805,7 +2018,8 @@ class _GroupRideDetailScreenState extends ConsumerState { ), ), onPressed: () async { - ref.read(activeLiveRideSharingIdProvider.notifier).state = null; + ref.read(activeLiveRideSharingIdProvider.notifier).state = + null; await service.clearLivePresence( callerUid: currentUid, groupRideId: ride.id, @@ -1819,6 +2033,7 @@ class _GroupRideDetailScreenState extends ConsumerState { SizedBox( width: double.infinity, child: ElevatedButton.icon( + key: const ValueKey('open_live_group_ride_map_button'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF00E5FF), foregroundColor: Colors.black, @@ -1870,7 +2085,9 @@ class _GroupRideDetailScreenState extends ConsumerState { children: [ Icon( p.isFresh ? Icons.circle : Icons.circle_outlined, - color: p.isFresh ? Colors.greenAccent : Colors.amber, + color: p.isFresh + ? Colors.greenAccent + : Colors.amber, size: 10, ), const SizedBox(width: 8), @@ -1896,13 +2113,19 @@ class _GroupRideDetailScreenState extends ConsumerState { vertical: 2, ), decoration: BoxDecoration( - color: (p.isFresh ? Colors.greenAccent : Colors.amber).withValues(alpha: 0.15), + color: + (p.isFresh + ? Colors.greenAccent + : Colors.amber) + .withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), ), child: Text( p.isFresh ? 'FRESH' : 'STALE', style: TextStyle( - color: p.isFresh ? Colors.greenAccent : Colors.amber, + color: p.isFresh + ? Colors.greenAccent + : Colors.amber, fontSize: 9, fontWeight: FontWeight.w900, ), @@ -1920,7 +2143,10 @@ class _GroupRideDetailScreenState extends ConsumerState { child: SizedBox( width: 14, height: 14, - child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF00E5FF)), + child: CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xFF00E5FF), + ), ), ), ), @@ -1929,7 +2155,11 @@ class _GroupRideDetailScreenState extends ConsumerState { const SizedBox(height: 12), const Text( 'Stage 6.1 Privacy Freeze: Ephemeral location is isolated to /group_rides/{id}/live_presence with 120s TTL and auto-purges on ride completion. Moving radar map and rider dots will be rendered in Stage 7.', - style: TextStyle(color: Colors.white38, fontSize: 10, height: 1.3), + style: TextStyle( + color: Colors.white38, + fontSize: 10, + height: 1.3, + ), ), ], ), diff --git a/lib/features/group_rides/group_rides_hub_screen.dart b/lib/features/group_rides/group_rides_hub_screen.dart index f8efb87..32b97a4 100644 --- a/lib/features/group_rides/group_rides_hub_screen.dart +++ b/lib/features/group_rides/group_rides_hub_screen.dart @@ -72,6 +72,7 @@ class _GroupRidesHubScreenState extends ConsumerState actions: [ if (isAuthed) IconButton( + key: const ValueKey('group_rides_create_appbar_button'), icon: const Icon(Icons.add, color: Color(0xFF00E5FF)), tooltip: 'Create Group Ride', onPressed: () => context.push('/group-rides/create'), @@ -89,6 +90,7 @@ class _GroupRidesHubScreenState extends ConsumerState ), floatingActionButton: isAuthed ? FloatingActionButton.extended( + key: const ValueKey('create_group_ride_fab'), backgroundColor: const Color(0xFF00E5FF), foregroundColor: Colors.black, icon: const Icon(Icons.add_road), @@ -134,15 +136,21 @@ class _GroupRidesHubScreenState extends ConsumerState const Text( 'To organize group rides, invite fellow riders, and coordinate meeting points, sign in with your permanent MotorSignal account.', 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), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF00E5FF), foregroundColor: Colors.black, - padding: - const EdgeInsets.symmetric(horizontal: 24, vertical: 14), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 14, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), @@ -161,7 +169,10 @@ class _GroupRidesHubScreenState extends ConsumerState } Widget _buildUpcomingTab( - BuildContext context, WidgetRef ref, String currentUid) { + BuildContext context, + WidgetRef ref, + String currentUid, + ) { final ridesAsync = ref.watch(upcomingGroupRidesProvider); return ridesAsync.when( @@ -190,20 +201,26 @@ class _GroupRidesHubScreenState extends ConsumerState child: CircularProgressIndicator(color: Color(0xFF00E5FF)), ), error: (e, _) => Center( - child: Text('Error loading rides: $e', - style: const TextStyle(color: Colors.redAccent)), + child: Text( + 'Error loading rides: $e', + style: const TextStyle(color: Colors.redAccent), + ), ), ); } Widget _buildMyRidesTab( - BuildContext context, WidgetRef ref, String currentUid) { + BuildContext context, + WidgetRef ref, + String currentUid, + ) { final ridesAsync = ref.watch(upcomingGroupRidesProvider); return ridesAsync.when( data: (rides) { - final myRides = - rides.where((r) => r.organizerUid == currentUid).toList(); + final myRides = rides + .where((r) => r.organizerUid == currentUid) + .toList(); if (myRides.isEmpty) { return _buildEmptyState( @@ -229,8 +246,10 @@ class _GroupRidesHubScreenState extends ConsumerState child: CircularProgressIndicator(color: Color(0xFF00E5FF)), ), error: (e, _) => Center( - child: Text('Error: $e', - style: const TextStyle(color: Colors.redAccent)), + child: Text( + 'Error: $e', + style: const TextStyle(color: Colors.redAccent), + ), ), ); } @@ -283,7 +302,10 @@ class _GroupRidesHubScreenState extends ConsumerState } Widget _buildRideCard( - BuildContext context, GroupRide ride, String currentUid) { + BuildContext context, + GroupRide ride, + String currentUid, + ) { final isOrganizer = ride.organizerUid == currentUid; return Card( @@ -298,6 +320,7 @@ class _GroupRidesHubScreenState extends ConsumerState ), ), child: InkWell( + key: ValueKey('group_ride_card_${ride.id}'), borderRadius: BorderRadius.circular(12), onTap: () => context.push('/group-rides/${ride.id}'), child: Padding( @@ -308,8 +331,10 @@ class _GroupRidesHubScreenState extends ConsumerState Row( children: [ Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), decoration: BoxDecoration( color: ride.isCanceled ? Colors.redAccent.withValues(alpha: 0.2) @@ -331,7 +356,9 @@ class _GroupRidesHubScreenState extends ConsumerState if (isOrganizer) Container( padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), + horizontal: 6, + vertical: 2, + ), decoration: BoxDecoration( color: Colors.amber.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(4), @@ -348,8 +375,11 @@ class _GroupRidesHubScreenState extends ConsumerState const Spacer(), Row( children: [ - const Icon(Icons.people_outline, - size: 16, color: Colors.white60), + const Icon( + Icons.people_outline, + size: 16, + color: Colors.white60, + ), const SizedBox(width: 4), Text( '${ride.participantCount}${ride.participantLimit != null ? '/${ride.participantLimit}' : ''}', @@ -375,7 +405,11 @@ class _GroupRidesHubScreenState extends ConsumerState const SizedBox(height: 6), Row( children: [ - const Icon(Icons.schedule, size: 14, color: Color(0xFF00E5FF)), + const Icon( + Icons.schedule, + size: 14, + color: Color(0xFF00E5FF), + ), const SizedBox(width: 6), Text( 'Meet: ${_formatDateTime(ride.scheduledStartAt)} • KSU: ${_formatTime(ride.departureAt)}', @@ -390,8 +424,11 @@ class _GroupRidesHubScreenState extends ConsumerState const SizedBox(height: 4), Row( children: [ - const Icon(Icons.place_outlined, - size: 14, color: Colors.white54), + const Icon( + Icons.place_outlined, + size: 14, + color: Colors.white54, + ), const SizedBox(width: 6), Expanded( child: Text( @@ -409,8 +446,11 @@ class _GroupRidesHubScreenState extends ConsumerState const SizedBox(height: 4), Row( children: [ - const Icon(Icons.flag_outlined, - size: 14, color: Color(0xFFFFB300)), + const Icon( + Icons.flag_outlined, + size: 14, + color: Color(0xFFFFB300), + ), const SizedBox(width: 6), Expanded( child: Text( diff --git a/lib/features/group_rides/live_group_ride_screen.dart b/lib/features/group_rides/live_group_ride_screen.dart index c9c33ae..1574264 100644 --- a/lib/features/group_rides/live_group_ride_screen.dart +++ b/lib/features/group_rides/live_group_ride_screen.dart @@ -49,14 +49,18 @@ class _LiveGroupRideScreenState extends ConsumerState @override Widget build(BuildContext context) { final rideAsync = ref.watch(groupRideStreamProvider(widget.rideId)); - final participantsAsync = - ref.watch(groupRideParticipantsProvider(widget.rideId)); - final presencesAsync = - ref.watch(groupRideLivePresenceProvider(widget.rideId)); - final broadcastState = - ref.watch(liveBroadcastControllerProvider(widget.rideId)); - final broadcastController = - ref.read(liveBroadcastControllerProvider(widget.rideId).notifier); + final participantsAsync = ref.watch( + groupRideParticipantsProvider(widget.rideId), + ); + final presencesAsync = ref.watch( + groupRideLivePresenceProvider(widget.rideId), + ); + final broadcastState = ref.watch( + liveBroadcastControllerProvider(widget.rideId), + ); + final broadcastController = ref.read( + liveBroadcastControllerProvider(widget.rideId).notifier, + ); final user = ref.watch(currentUserProvider); final currentUid = user?.uid; @@ -78,7 +82,8 @@ class _LiveGroupRideScreenState extends ConsumerState ); } - final isOrganizer = currentUid != null && ride.organizerUid == currentUid; + final isOrganizer = + currentUid != null && ride.organizerUid == currentUid; // Check if ride was closed if (ride.isCompleted || ride.isCanceled) { @@ -103,7 +108,9 @@ class _LiveGroupRideScreenState extends ConsumerState children: [ Icon( ride.isCompleted ? Icons.flag : Icons.cancel, - color: ride.isCompleted ? Colors.greenAccent : Colors.redAccent, + color: ride.isCompleted + ? Colors.greenAccent + : Colors.redAccent, size: 48, ), const SizedBox(height: 16), @@ -136,28 +143,32 @@ class _LiveGroupRideScreenState extends ConsumerState return participantsAsync.when( data: (participants) { - final acceptedParticipants = - participants.where((p) => p.status == ParticipantStatus.accepted).toList(); + final acceptedParticipants = participants + .where((p) => p.status == ParticipantStatus.accepted) + .toList(); return presencesAsync.when( data: (presences) { // Determine Ride Lead (first participant with lead role, or organizer) GroupRideParticipant? leadParticipant; try { - leadParticipant = acceptedParticipants - .firstWhere((p) => p.role == GroupRideRole.lead); + leadParticipant = acceptedParticipants.firstWhere( + (p) => p.role == GroupRideRole.lead, + ); } catch (_) { try { - leadParticipant = acceptedParticipants - .firstWhere((p) => p.role == GroupRideRole.organizer); + leadParticipant = acceptedParticipants.firstWhere( + (p) => p.role == GroupRideRole.organizer, + ); } catch (_) {} } LiveGroupPresence? leadPresence; if (leadParticipant != null) { try { - leadPresence = presences - .firstWhere((p) => p.userId == leadParticipant!.userId); + leadPresence = presences.firstWhere( + (p) => p.userId == leadParticipant!.userId, + ); } catch (_) {} } @@ -165,7 +176,8 @@ class _LiveGroupRideScreenState extends ConsumerState LiveGroupPresence? regroupCaller; try { regroupCaller = presences.firstWhere( - (p) => p.status == LivePresenceStatus.regrouping && p.isFresh, + (p) => + p.status == LivePresenceStatus.regrouping && p.isFresh, ); } catch (_) {} @@ -246,7 +258,8 @@ class _LiveGroupRideScreenState extends ConsumerState context: context, caller: regroupCaller, participants: acceptedParticipants, - isLeadOrOrganizer: isOrganizer || + isLeadOrOrganizer: + isOrganizer || (leadParticipant != null && leadParticipant.userId == currentUid), controller: broadcastController, @@ -304,8 +317,10 @@ class _LiveGroupRideScreenState extends ConsumerState error: (e, _) => Scaffold( backgroundColor: const Color(0xFF0D0E11), body: Center( - child: Text('Error loading live presence: $e', - style: const TextStyle(color: Colors.redAccent)), + child: Text( + 'Error loading live presence: $e', + style: const TextStyle(color: Colors.redAccent), + ), ), ), ); @@ -319,8 +334,10 @@ class _LiveGroupRideScreenState extends ConsumerState error: (e, _) => Scaffold( backgroundColor: const Color(0xFF0D0E11), body: Center( - child: Text('Error loading participants: $e', - style: const TextStyle(color: Colors.redAccent)), + child: Text( + 'Error loading participants: $e', + style: const TextStyle(color: Colors.redAccent), + ), ), ), ); @@ -334,8 +351,10 @@ class _LiveGroupRideScreenState extends ConsumerState error: (e, _) => Scaffold( backgroundColor: const Color(0xFF0D0E11), body: Center( - child: Text('Error loading ride: $e', - style: const TextStyle(color: Colors.redAccent)), + child: Text( + 'Error loading ride: $e', + style: const TextStyle(color: Colors.redAccent), + ), ), ), ); @@ -406,7 +425,10 @@ class _LiveGroupRideScreenState extends ConsumerState actions: [ if (isOrganizer) IconButton( - icon: const Icon(Icons.stop_circle_outlined, color: Colors.redAccent), + icon: const Icon( + Icons.stop_circle_outlined, + color: Colors.redAccent, + ), tooltip: 'End Ride', onPressed: () => _confirmEndRide(context, ride, currentUid), ), @@ -435,7 +457,9 @@ class _LiveGroupRideScreenState extends ConsumerState String callerName = caller.userId; try { 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 (_) {} return Container( @@ -450,7 +474,11 @@ class _LiveGroupRideScreenState extends ConsumerState ), child: Row( 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), Expanded( child: Column( @@ -484,7 +512,10 @@ class _LiveGroupRideScreenState extends ConsumerState minimumSize: Size.zero, ), 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 TextButton( @@ -495,7 +526,10 @@ class _LiveGroupRideScreenState extends ConsumerState minimumSize: Size.zero, ), 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 minimumSize: Size.zero, ), 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 final isSelected = currentStatus == status; Color activeColor = const Color(0xFF00E5FF); - if (status == LivePresenceStatus.needFuel) activeColor = Colors.orangeAccent; - if (status == LivePresenceStatus.regrouping) activeColor = Colors.amber; - if (status == LivePresenceStatus.arrived) activeColor = Colors.greenAccent; + if (status == LivePresenceStatus.needFuel) + activeColor = Colors.orangeAccent; + if (status == LivePresenceStatus.regrouping) + activeColor = Colors.amber; + if (status == LivePresenceStatus.arrived) + activeColor = Colors.greenAccent; return InkWell( borderRadius: BorderRadius.circular(6), @@ -605,7 +646,9 @@ class _LiveGroupRideScreenState extends ConsumerState label, style: TextStyle( color: isSelected ? activeColor : Colors.white70, - fontWeight: isSelected ? FontWeight.bold : FontWeight.w500, + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.w500, fontSize: 11, ), ), @@ -662,17 +705,17 @@ class _LiveGroupRideScreenState extends ConsumerState Text( broadcastState.isBroadcasting ? (broadcastState.isPaused - ? 'SHARING PAUSED' - : (broadcastState.isBackground - ? 'BACKGROUND SHARING' - : 'SHARING LIVE')) + ? 'SHARING PAUSED' + : (broadcastState.isBackground + ? 'BACKGROUND SHARING' + : 'SHARING LIVE')) : 'GHOST MODE', overflow: TextOverflow.ellipsis, style: TextStyle( color: broadcastState.isBroadcasting ? (broadcastState.isPaused - ? Colors.amber - : const Color(0xFF00E5FF)) + ? Colors.amber + : const Color(0xFF00E5FF)) : Colors.white60, fontWeight: FontWeight.bold, fontSize: 11, @@ -682,18 +725,22 @@ class _LiveGroupRideScreenState extends ConsumerState Text( broadcastState.isBroadcasting ? (broadcastState.isPaused - ? 'Reconnecting (${broadcastState.retryCount})...' - : (broadcastState.isBackground - ? 'Active via foreground service' - : 'Broadcasting to riders')) + ? 'Reconnecting (${broadcastState.retryCount})...' + : (broadcastState.isBackground + ? 'Active via foreground service' + : 'Broadcasting to riders')) : 'Location paused', overflow: TextOverflow.ellipsis, - style: const TextStyle(color: Colors.white38, fontSize: 10), + style: const TextStyle( + color: Colors.white38, + fontSize: 10, + ), ), ], ), ), Switch( + key: const ValueKey('live_ride_broadcast_switch'), value: broadcastState.isOptedIn, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, activeThumbColor: const Color(0xFF00E5FF), @@ -707,12 +754,17 @@ class _LiveGroupRideScreenState extends ConsumerState // Recenter Map Button IconButton( + key: const ValueKey('live_ride_recenter_button'), style: IconButton.styleFrom( backgroundColor: Colors.white10, padding: const EdgeInsets.all(8), 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', onPressed: () { if (broadcastState.lastPosition != null) { @@ -738,6 +790,7 @@ class _LiveGroupRideScreenState extends ConsumerState // Roster Opener Button ElevatedButton.icon( + key: const ValueKey('live_ride_roster_button'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15), foregroundColor: const Color(0xFF00E5FF), @@ -877,7 +930,10 @@ class _LiveGroupRideScreenState extends ConsumerState participant.riderDisplayName.isNotEmpty ? participant.riderDisplayName[0].toUpperCase() : '?', - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), ), if (isLive) @@ -908,7 +964,9 @@ class _LiveGroupRideScreenState extends ConsumerState : participant.displayHandle, overflow: TextOverflow.ellipsis, style: TextStyle( - color: isCaller ? const Color(0xFF00E5FF) : Colors.white, + color: isCaller + ? const Color(0xFF00E5FF) + : Colors.white, fontWeight: FontWeight.bold, fontSize: 13, ), @@ -917,9 +975,14 @@ class _LiveGroupRideScreenState extends ConsumerState if (participant.role != GroupRideRole.participant) ...[ const SizedBox(width: 6), Container( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 1, + ), decoration: BoxDecoration( - color: _roleColor(participant.role).withValues(alpha: 0.2), + color: _roleColor( + participant.role, + ).withValues(alpha: 0.2), borderRadius: BorderRadius.circular(3), ), child: Text( @@ -954,10 +1017,16 @@ class _LiveGroupRideScreenState extends ConsumerState ), if (distanceString.isNotEmpty) ...[ - const Text(' • ', style: TextStyle(color: Colors.white24, fontSize: 10)), + const Text( + ' • ', + style: TextStyle(color: Colors.white24, fontSize: 10), + ), Text( 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 ), const SizedBox(width: 6), 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), padding: EdgeInsets.zero, tooltip: 'Center on rider', - onPressed: () => onCenter(AppLatLng(presence.latitude, presence.longitude)), + onPressed: () => + onCenter(AppLatLng(presence.latitude, presence.longitude)), ), ] else const Text( @@ -1029,7 +1103,9 @@ class _LiveGroupRideScreenState extends ConsumerState } LiveGroupPresence? _findPresence( - List presences, String uid) { + List presences, + String uid, + ) { try { return presences.firstWhere((p) => p.userId == uid); } catch (_) { @@ -1044,7 +1120,8 @@ class _LiveGroupRideScreenState extends ConsumerState required GroupRideLocation meetup, }) { 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) { return AppLatLng(leadPresence.latitude, leadPresence.longitude); } @@ -1066,7 +1143,10 @@ class _LiveGroupRideScreenState extends ConsumerState markers.add( AppMapMarker( id: 'meetup_point', - position: AppLatLng(ride.meetingPoint.latitude, ride.meetingPoint.longitude), + position: AppLatLng( + ride.meetingPoint.latitude, + ride.meetingPoint.longitude, + ), label: ride.meetingPoint.displayName.isNotEmpty ? ride.meetingPoint.displayName : 'Meetup', @@ -1076,7 +1156,11 @@ class _LiveGroupRideScreenState extends ConsumerState color: Colors.greenAccent, shape: BoxShape.circle, 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), @@ -1106,7 +1190,11 @@ class _LiveGroupRideScreenState extends ConsumerState color: Color(0xFF00E5FF), shape: BoxShape.circle, boxShadow: [ - BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)), + BoxShadow( + color: Colors.black45, + blurRadius: 4, + offset: Offset(0, 2), + ), ], ), child: Text( @@ -1136,14 +1224,20 @@ class _LiveGroupRideScreenState extends ConsumerState AppMapMarker( id: 'stop_$i', 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( padding: const EdgeInsets.all(5), decoration: const BoxDecoration( color: Colors.blueAccent, shape: BoxShape.circle, boxShadow: [ - BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)), + BoxShadow( + color: Colors.black45, + blurRadius: 4, + offset: Offset(0, 2), + ), ], ), child: Text( @@ -1173,7 +1267,10 @@ class _LiveGroupRideScreenState extends ConsumerState markers.add( AppMapMarker( id: 'destination_point', - position: AppLatLng(ride.destination!.latitude, ride.destination!.longitude), + position: AppLatLng( + ride.destination!.latitude, + ride.destination!.longitude, + ), label: ride.destination!.displayName.isNotEmpty ? ride.destination!.displayName : 'Destination', @@ -1183,10 +1280,18 @@ class _LiveGroupRideScreenState extends ConsumerState color: Colors.redAccent, shape: BoxShape.circle, 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 p = participants.firstWhere((item) => item.userId == presence.userId); } 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 isCaller = presence.userId == currentUid; @@ -1207,8 +1313,10 @@ class _LiveGroupRideScreenState extends ConsumerState Color markerColor = const Color(0xFF00E5FF); if (isLead) markerColor = const Color(0xFFFFD700); // Gold for lead if (isSweep) markerColor = Colors.orangeAccent; // Orange for sweep - if (presence.status == LivePresenceStatus.regrouping) markerColor = Colors.amber; - if (presence.status == LivePresenceStatus.needFuel) markerColor = Colors.deepOrangeAccent; + if (presence.status == LivePresenceStatus.regrouping) + markerColor = Colors.amber; + if (presence.status == LivePresenceStatus.needFuel) + markerColor = Colors.deepOrangeAccent; final isStale = presence.isStale; final opacity = isStale ? 0.4 : 1.0; @@ -1256,14 +1364,19 @@ class _LiveGroupRideScreenState extends ConsumerState } void _confirmEndRide( - BuildContext context, GroupRide ride, String? currentUid) { + BuildContext context, + GroupRide ride, + String? currentUid, + ) { if (currentUid == null) return; showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF161922), - title: const Text('End Group Ride?', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + title: const Text( + 'End Group Ride?', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + ), content: const Text( 'This will conclude the group ride for all participants and terminate live location sharing.', style: TextStyle(color: Colors.white70), @@ -1271,7 +1384,10 @@ class _LiveGroupRideScreenState extends ConsumerState actions: [ TextButton( onPressed: () => Navigator.pop(ctx), - child: const Text('CANCEL', style: TextStyle(color: Colors.white60)), + child: const Text( + 'CANCEL', + style: TextStyle(color: Colors.white60), + ), ), ElevatedButton( style: ElevatedButton.styleFrom( diff --git a/lib/features/history/history_screen.dart b/lib/features/history/history_screen.dart index fa2688f..f119ab9 100644 --- a/lib/features/history/history_screen.dart +++ b/lib/features/history/history_screen.dart @@ -13,7 +13,8 @@ class HistoryScreen extends ConsumerStatefulWidget { } class _HistoryScreenState extends ConsumerState { - int? _selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID + int? + _selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID @override Widget build(BuildContext context) { @@ -42,9 +43,14 @@ class _HistoryScreenState extends ConsumerState { // Filter logic if (_selectedMotorcycleId == -1 && defaultBike != null) { - rides = rides.where((r) => r.motorcycleId == defaultBike.id).toList(); - } else if (_selectedMotorcycleId != null && _selectedMotorcycleId! > 0) { - rides = rides.where((r) => r.motorcycleId == _selectedMotorcycleId).toList(); + rides = rides + .where((r) => r.motorcycleId == defaultBike.id) + .toList(); + } else if (_selectedMotorcycleId != null && + _selectedMotorcycleId! > 0) { + rides = rides + .where((r) => r.motorcycleId == _selectedMotorcycleId) + .toList(); } // Sort by start time descending @@ -64,14 +70,21 @@ class _HistoryScreenState extends ConsumerState { itemCount: rides.length, itemBuilder: (context, 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}' : null; return Card( elevation: 4, - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + margin: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 4, + ), child: InkWell( + key: ValueKey('history_ride_card_${ride.id}'), onTap: () { Navigator.of(context).push( MaterialPageRoute( @@ -92,23 +105,39 @@ class _HistoryScreenState extends ConsumerState { children: [ Text( 'Ride ${ride.id}', - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), ), if (bikeName != null) ...[ const SizedBox(width: 8), Flexible( child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + padding: + const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), decoration: BoxDecoration( - color: Colors.cyanAccent.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.cyanAccent.withValues(alpha: 0.5)), + color: Colors.cyanAccent + .withValues(alpha: 0.15), + borderRadius: + BorderRadius.circular(4), + border: Border.all( + color: Colors.cyanAccent + .withValues(alpha: 0.5), + ), ), child: Text( bikeName, maxLines: 1, 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 { ), const SizedBox(width: 8), Text( - ride.startTime.toLocal().toString().split('.')[0], - style: const TextStyle(color: Colors.grey, fontSize: 11), + ride.startTime.toLocal().toString().split( + '.', + )[0], + style: const TextStyle( + color: Colors.grey, + fontSize: 11, + ), ), ], ), const Divider(), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ - _buildStatCol('TRIP', '${ride.distanceMiles.toStringAsFixed(1)} mi'), - _buildStatCol('TIME', '${(ride.movingTime / 60).toStringAsFixed(0)} min'), - _buildStatCol('TOP SPD', '${ride.topSpeedMph.toStringAsFixed(0)} mph'), + _buildStatCol( + 'TRIP', + '${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), Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, children: [ - _buildStatCol('MAX LEAN', '${math.max(ride.maxLeanLeft, ride.maxLeanRight).toStringAsFixed(0)}°'), - _buildStatCol('HARD BRK', '${ride.maxBrakingG.toStringAsFixed(2)}G'), - _buildStatCol('ELEV GAIN', '${ride.elevationGain.toStringAsFixed(0)}m'), + _buildStatCol( + 'MAX LEAN', + '${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 { ); } - Widget _buildFilterBar(List bikes, GarageMotorcycle? defaultBike) { + Widget _buildFilterBar( + List bikes, + GarageMotorcycle? defaultBike, + ) { if (bikes.isEmpty) return const SizedBox(); return Padding( @@ -188,10 +245,12 @@ class _HistoryScreenState extends ConsumerState { value: -1, child: Text('Current (${defaultBike.modelName})'), ), - ...bikes.map((b) => DropdownMenuItem( - value: b.id, - child: Text('${b.year} ${b.makeName} ${b.modelName}'), - )), + ...bikes.map( + (b) => DropdownMenuItem( + value: b.id, + child: Text('${b.year} ${b.makeName} ${b.modelName}'), + ), + ), ], onChanged: (val) { setState(() { @@ -212,7 +271,10 @@ class _HistoryScreenState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ 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), + ), ], ); } diff --git a/lib/features/routes/route_detail_screen.dart b/lib/features/routes/route_detail_screen.dart index dc1ca60..ef49978 100644 --- a/lib/features/routes/route_detail_screen.dart +++ b/lib/features/routes/route_detail_screen.dart @@ -29,7 +29,10 @@ class _RouteDetailScreenState extends ConsumerState { borderRadius: BorderRadius.circular(16), 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( 'Are you sure you want to delete "${route.title}"? This cannot be undone.', style: const TextStyle(color: Colors.white70), @@ -37,7 +40,10 @@ class _RouteDetailScreenState extends ConsumerState { actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), - child: const Text('CANCEL', style: TextStyle(color: Colors.white54)), + child: const Text( + 'CANCEL', + style: TextStyle(color: Colors.white54), + ), ), ElevatedButton( style: ElevatedButton.styleFrom( @@ -58,17 +64,17 @@ class _RouteDetailScreenState extends ConsumerState { final service = ref.read(routeServiceProvider); await service.deleteRoute(widget.routeId); if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Route deleted.')), - ); + 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')), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Failed to delete route: $e'))); } } } @@ -132,8 +138,11 @@ class _RouteDetailScreenState extends ConsumerState { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.error_outline, - size: 48, color: Color(0xFFFF5252)), + const Icon( + Icons.error_outline, + size: 48, + color: Color(0xFFFF5252), + ), const SizedBox(height: 12), Text( 'Could not load route: $err', @@ -160,7 +169,8 @@ class _RouteDetailScreenState extends ConsumerState { final markers = route.stops.map((stop) { final isFirst = stop.orderIndex == 0; final isLast = - stop.orderIndex == route.stops.length - 1 && route.stops.length > 1; + stop.orderIndex == route.stops.length - 1 && + route.stops.length > 1; return AppMapMarker( id: stop.id, @@ -172,12 +182,15 @@ class _RouteDetailScreenState extends ConsumerState { color: isFirst ? const Color(0xFF00E676) : isLast - ? const Color(0xFFFF5252) - : const Color(0xFF00E5FF), + ? const Color(0xFFFF5252) + : const Color(0xFF00E5FF), shape: BoxShape.circle, boxShadow: const [ BoxShadow( - color: Colors.black54, blurRadius: 4, spreadRadius: 1), + color: Colors.black54, + blurRadius: 4, + spreadRadius: 1, + ), ], ), child: Text( @@ -216,7 +229,9 @@ class _RouteDetailScreenState extends ConsumerState { final mapCenter = route.stops.isNotEmpty ? AppLatLng( - route.stops.first.latitude, route.stops.first.longitude) + route.stops.first.latitude, + route.stops.first.longitude, + ) : const AppLatLng(37.7749, -122.4194); return SingleChildScrollView( @@ -240,9 +255,13 @@ class _RouteDetailScreenState extends ConsumerState { left: 8, child: Container( padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 4), + horizontal: 10, + vertical: 4, + ), decoration: BoxDecoration( - color: const Color(0xFF13151A).withValues(alpha: 0.9), + color: const Color( + 0xFF13151A, + ).withValues(alpha: 0.9), borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFF2A2E3D)), ), @@ -370,35 +389,54 @@ class _RouteDetailScreenState extends ConsumerState { children: [ Expanded( child: OutlinedButton.icon( + key: const ValueKey('route_detail_edit_button'), onPressed: () { - context.push('/routes/planner?routeId=${route.id}'); + 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))), + 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)), + color: Color(0xFF00E5FF), + ), padding: const EdgeInsets.symmetric( - vertical: 10), + 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))), + 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)), + color: Color(0xFFFF5252), + ), padding: const EdgeInsets.symmetric( - vertical: 10, horizontal: 16), + vertical: 10, + horizontal: 16, + ), ), ), ], @@ -409,14 +447,23 @@ class _RouteDetailScreenState extends ConsumerState { // 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}'); + '/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))), + 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), @@ -445,7 +492,8 @@ class _RouteDetailScreenState extends ConsumerState { itemBuilder: (context, index) { final stop = route.stops[index]; final isFirst = index == 0; - final isLast = index == route.stops.length - 1 && + final isLast = + index == route.stops.length - 1 && route.stops.length > 1; return Container( @@ -454,7 +502,9 @@ class _RouteDetailScreenState extends ConsumerState { decoration: BoxDecoration( color: const Color(0xFF13151A), borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFF2A2E3D)), + border: Border.all( + color: const Color(0xFF2A2E3D), + ), ), child: Row( children: [ @@ -465,8 +515,8 @@ class _RouteDetailScreenState extends ConsumerState { color: isFirst ? const Color(0xFF00E676) : isLast - ? const Color(0xFFFF5252) - : const Color(0xFF00E5FF), + ? const Color(0xFFFF5252) + : const Color(0xFF00E5FF), shape: BoxShape.circle, ), alignment: Alignment.center, @@ -487,9 +537,11 @@ class _RouteDetailScreenState extends ConsumerState { children: [ Row( children: [ - Icon(_getIconForStopType(stop.stopType), - size: 14, - color: const Color(0xFF00E5FF)), + Icon( + _getIconForStopType(stop.stopType), + size: 14, + color: const Color(0xFF00E5FF), + ), const SizedBox(width: 4), Flexible( child: Text( @@ -540,7 +592,9 @@ class _RouteDetailScreenState extends ConsumerState { backgroundColor: const Color(0xFF00E5FF), foregroundColor: Colors.black, padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 6), + horizontal: 10, + vertical: 6, + ), textStyle: const TextStyle( fontSize: 11, fontWeight: FontWeight.bold, @@ -557,6 +611,9 @@ class _RouteDetailScreenState extends ConsumerState { // 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( diff --git a/lib/features/routes/route_planner_screen.dart b/lib/features/routes/route_planner_screen.dart index 5a05730..8753c44 100644 --- a/lib/features/routes/route_planner_screen.dart +++ b/lib/features/routes/route_planner_screen.dart @@ -108,7 +108,9 @@ class _RoutePlannerScreenState extends ConsumerState { }) { final state = ref.read(routePlannerNotifierProvider); 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) { ScaffoldMessenger.of(context).showSnackBar( @@ -145,10 +147,9 @@ class _RoutePlannerScreenState extends ConsumerState { : (initialLng?.toStringAsFixed(6) ?? ''), ); final notesCtrl = TextEditingController(text: existingStop?.notes ?? ''); - RouteStopType selectedType = existingStop?.stopType ?? - (state.stops.isEmpty - ? RouteStopType.origin - : RouteStopType.waypoint); + RouteStopType selectedType = + existingStop?.stopType ?? + (state.stops.isEmpty ? RouteStopType.origin : RouteStopType.waypoint); showDialog( context: context, @@ -206,8 +207,11 @@ class _RoutePlannerScreenState extends ConsumerState { value: type, child: Row( children: [ - Icon(_getIconForStopType(type), - color: const Color(0xFF00E5FF), size: 18), + Icon( + _getIconForStopType(type), + color: const Color(0xFF00E5FF), + size: 18, + ), const SizedBox(width: 8), Expanded( child: Text( @@ -235,7 +239,10 @@ class _RoutePlannerScreenState extends ConsumerState { decimal: true, signed: true, ), - style: const TextStyle(color: Colors.white, fontSize: 13), + style: const TextStyle( + color: Colors.white, + fontSize: 13, + ), decoration: const InputDecoration( labelText: 'Latitude *', labelStyle: TextStyle(color: Colors.white60), @@ -252,7 +259,10 @@ class _RoutePlannerScreenState extends ConsumerState { decimal: true, signed: true, ), - style: const TextStyle(color: Colors.white, fontSize: 13), + style: const TextStyle( + color: Colors.white, + fontSize: 13, + ), decoration: const InputDecoration( labelText: 'Longitude *', labelStyle: TextStyle(color: Colors.white60), @@ -281,7 +291,10 @@ class _RoutePlannerScreenState extends ConsumerState { actions: [ TextButton( onPressed: () => Navigator.pop(dialogCtx), - child: const Text('CANCEL', style: TextStyle(color: Colors.white54)), + child: const Text( + 'CANCEL', + style: TextStyle(color: Colors.white54), + ), ), ElevatedButton( style: ElevatedButton.styleFrom( @@ -295,24 +308,32 @@ class _RoutePlannerScreenState extends ConsumerState { if (name.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Please provide a stop name.')), + const SnackBar( + content: Text('Please provide a stop name.'), + ), ); return; } if (lat == null || lat < -90.0 || lat > 90.0) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Invalid latitude (-90 to 90).')), + const SnackBar( + content: Text('Invalid latitude (-90 to 90).'), + ), ); return; } if (lng == null || lng < -180.0 || lng > 180.0) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Invalid longitude (-180 to 180).')), + const SnackBar( + content: Text('Invalid longitude (-180 to 180).'), + ), ); return; } - final notifier = ref.read(routePlannerNotifierProvider.notifier); + final notifier = ref.read( + routePlannerNotifierProvider.notifier, + ); if (existingStop != null) { notifier.updateStop( existingStop.copyWith( @@ -362,7 +383,9 @@ class _RoutePlannerScreenState extends ConsumerState { final isPro = ref.read(isProSubscriberProvider); final state = ref.read(routePlannerNotifierProvider); - final maxAllowed = isPro ? RouteService.maxTechnicalStops : RouteService.freeStopCap; + final maxAllowed = isPro + ? RouteService.maxTechnicalStops + : RouteService.freeStopCap; if (state.stops.length > maxAllowed) { setState(() { @@ -447,13 +470,17 @@ class _RoutePlannerScreenState extends ConsumerState { if (!_initialized) { return const Scaffold( backgroundColor: Color(0xFF0D0E11), - body: Center(child: CircularProgressIndicator(color: Color(0xFF00E5FF))), + body: Center( + child: CircularProgressIndicator(color: Color(0xFF00E5FF)), + ), ); } final state = ref.watch(routePlannerNotifierProvider); final isPro = ref.watch(isProSubscriberProvider); - final maxAllowed = isPro ? RouteService.maxTechnicalStops : RouteService.freeStopCap; + final maxAllowed = isPro + ? RouteService.maxTechnicalStops + : RouteService.freeStopCap; final isAtLimit = state.stops.length >= maxAllowed; // Center map on first stop or default @@ -464,7 +491,8 @@ class _RoutePlannerScreenState extends ConsumerState { // Build markers final markers = state.stops.map((stop) { final isFirst = stop.orderIndex == 0; - final isLast = stop.orderIndex == state.stops.length - 1 && state.stops.length > 1; + final isLast = + stop.orderIndex == state.stops.length - 1 && state.stops.length > 1; return AppMapMarker( id: stop.id, @@ -476,8 +504,8 @@ class _RoutePlannerScreenState extends ConsumerState { color: isFirst ? const Color(0xFF00E676) : isLast - ? const Color(0xFFFF5252) - : const Color(0xFF00E5FF), + ? const Color(0xFFFF5252) + : const Color(0xFF00E5FF), shape: BoxShape.circle, boxShadow: const [ BoxShadow(color: Colors.black54, blurRadius: 4, spreadRadius: 1), @@ -551,6 +579,7 @@ class _RoutePlannerScreenState extends ConsumerState { ) else TextButton.icon( + key: const ValueKey('route_planner_save_button'), onPressed: _saveRoute, icon: const Icon(Icons.save, color: Color(0xFF00E5FF), size: 18), label: const Text( @@ -574,11 +603,17 @@ class _RoutePlannerScreenState extends ConsumerState { Container( width: double.infinity, 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( children: [ - const Icon(Icons.error_outline, - color: Color(0xFFFF5252), size: 18), + const Icon( + Icons.error_outline, + color: Color(0xFFFF5252), + size: 18, + ), const SizedBox(width: 8), Expanded( child: Text( @@ -590,7 +625,11 @@ class _RoutePlannerScreenState extends ConsumerState { ), ), 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), padding: EdgeInsets.zero, constraints: const BoxConstraints(), @@ -618,7 +657,9 @@ class _RoutePlannerScreenState extends ConsumerState { right: 8, child: Container( padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 6), + horizontal: 10, + vertical: 6, + ), decoration: BoxDecoration( color: const Color(0xFF13151A).withValues(alpha: 0.9), borderRadius: BorderRadius.circular(8), @@ -643,11 +684,17 @@ class _RoutePlannerScreenState extends ConsumerState { const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), + horizontal: 6, + vertical: 2, + ), decoration: BoxDecoration( color: isAtLimit - ? const Color(0xFFFFB300).withValues(alpha: 0.2) - : const Color(0xFF00E5FF).withValues(alpha: 0.15), + ? const Color( + 0xFFFFB300, + ).withValues(alpha: 0.2) + : const Color( + 0xFF00E5FF, + ).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(4), ), child: Text( @@ -700,7 +747,10 @@ class _RoutePlannerScreenState extends ConsumerState { ), decoration: const InputDecoration( hintText: 'Route Title (e.g. Skyline Ridge Loop) *', - hintStyle: TextStyle(color: Colors.white30, fontSize: 14), + hintStyle: TextStyle( + color: Colors.white30, + fontSize: 14, + ), isDense: true, contentPadding: EdgeInsets.symmetric(vertical: 8), border: InputBorder.none, @@ -721,8 +771,11 @@ class _RoutePlannerScreenState extends ConsumerState { child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.visibility_outlined, - size: 14, color: Colors.white54), + const Icon( + Icons.visibility_outlined, + size: 14, + color: Colors.white54, + ), const SizedBox(width: 4), Text( _visibility.label, @@ -732,30 +785,36 @@ class _RoutePlannerScreenState extends ConsumerState { fontWeight: FontWeight.bold, ), ), - const Icon(Icons.arrow_drop_down, - size: 16, color: Color(0xFF00E5FF)), + const Icon( + Icons.arrow_drop_down, + size: 16, + color: Color(0xFF00E5FF), + ), ], ), ), - itemBuilder: (context) => RouteVisibility.values.map((v) { - return PopupMenuItem( - value: v, - child: Text( - v.label, - style: const TextStyle( - color: Colors.white, - fontSize: 13, - ), - ), - ); - }).toList(), + itemBuilder: (context) => + RouteVisibility.values.map((v) { + return PopupMenuItem( + value: v, + child: Text( + v.label, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + ), + ), + ); + }).toList(), ), if (!isPro && isAtLimit) TextButton( onPressed: () => context.push('/settings/pro'), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 4), + horizontal: 8, + vertical: 4, + ), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), @@ -778,7 +837,10 @@ class _RoutePlannerScreenState extends ConsumerState { // Stop List Controls (Add Stop / Add GPS / Reverse) Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), child: Wrap( spacing: 8, runSpacing: 8, @@ -789,6 +851,7 @@ class _RoutePlannerScreenState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ ElevatedButton.icon( + key: const ValueKey('route_planner_add_stop_button'), onPressed: () => _showAddOrEditStopDialog(), icon: const Icon(Icons.add_location_alt, size: 16), label: Text(isNarrow ? 'ADD' : 'ADD STOP'), @@ -796,7 +859,9 @@ class _RoutePlannerScreenState extends ConsumerState { backgroundColor: const Color(0xFF00E5FF), foregroundColor: Colors.black, padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 8), + horizontal: 10, + vertical: 8, + ), textStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, @@ -805,15 +870,25 @@ class _RoutePlannerScreenState extends ConsumerState { ), const SizedBox(width: 8), OutlinedButton.icon( + key: const ValueKey( + 'route_planner_current_gps_button', + ), onPressed: _addCurrentLocationStop, - icon: const Icon(Icons.my_location, - size: 16, color: Color(0xFF00E5FF)), - label: Text(isNarrow ? 'GPS' : 'CURRENT GPS', - style: const TextStyle(color: Color(0xFF00E5FF))), + icon: const Icon( + Icons.my_location, + size: 16, + color: Color(0xFF00E5FF), + ), + label: Text( + isNarrow ? 'GPS' : 'CURRENT GPS', + style: const TextStyle(color: Color(0xFF00E5FF)), + ), style: OutlinedButton.styleFrom( side: const BorderSide(color: Color(0xFF2A2E3D)), padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 8), + horizontal: 10, + vertical: 8, + ), textStyle: const TextStyle(fontSize: 12), ), ), @@ -821,15 +896,24 @@ class _RoutePlannerScreenState extends ConsumerState { ), if (state.stops.length >= 2) TextButton.icon( - onPressed: () => - ref.read(routePlannerNotifierProvider.notifier).reverseStops(), - icon: const Icon(Icons.swap_vert, - size: 16, color: Colors.white70), - label: const Text('REVERSE', - style: TextStyle(color: Colors.white70, fontSize: 11)), + key: const ValueKey('route_planner_reverse_button'), + onPressed: () => ref + .read(routePlannerNotifierProvider.notifier) + .reverseStops(), + icon: const Icon( + Icons.swap_vert, + size: 16, + color: Colors.white70, + ), + label: const Text( + 'REVERSE', + style: TextStyle(color: Colors.white70, fontSize: 11), + ), style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 4), + horizontal: 6, + vertical: 4, + ), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), @@ -845,27 +929,35 @@ class _RoutePlannerScreenState extends ConsumerState { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.alt_route, - size: 48, - color: Colors.white.withValues(alpha: 0.15)), + Icon( + Icons.alt_route, + size: 48, + color: Colors.white.withValues(alpha: 0.15), + ), const SizedBox(height: 12), const Text( 'No stops added yet.', style: TextStyle( - color: Colors.white54, fontSize: 14), + color: Colors.white54, + fontSize: 14, + ), ), const SizedBox(height: 6), const Text( 'Tap "Add Stop" or "Current GPS" to begin planning.', style: TextStyle( - color: Colors.white30, fontSize: 12), + color: Colors.white30, + fontSize: 12, + ), ), ], ), ) : ReorderableListView.builder( padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 4), + horizontal: 16, + vertical: 4, + ), itemCount: state.stops.length, // ignore: deprecated_member_use onReorder: (oldIdx, newIdx) { @@ -877,7 +969,8 @@ class _RoutePlannerScreenState extends ConsumerState { final stop = state.stops[index]; final isFirst = index == 0; final isLast = - index == state.stops.length - 1 && state.stops.length > 1; + index == state.stops.length - 1 && + state.stops.length > 1; return Container( key: ValueKey(stop.id), @@ -886,7 +979,9 @@ class _RoutePlannerScreenState extends ConsumerState { decoration: BoxDecoration( color: const Color(0xFF13151A), borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFF2A2E3D)), + border: Border.all( + color: const Color(0xFF2A2E3D), + ), ), child: Row( children: [ @@ -898,8 +993,8 @@ class _RoutePlannerScreenState extends ConsumerState { color: isFirst ? const Color(0xFF00E676) : isLast - ? const Color(0xFFFF5252) - : const Color(0xFF00E5FF), + ? const Color(0xFFFF5252) + : const Color(0xFF00E5FF), shape: BoxShape.circle, ), alignment: Alignment.center, @@ -917,7 +1012,8 @@ class _RoutePlannerScreenState extends ConsumerState { // Stop Details Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, children: [ Row( children: [ @@ -966,8 +1062,11 @@ class _RoutePlannerScreenState extends ConsumerState { // Actions: Handoff navigation, Edit, Delete IconButton( - icon: const Icon(Icons.navigation, - color: Color(0xFF00E5FF), size: 18), + icon: const Icon( + Icons.navigation, + color: Color(0xFF00E5FF), + size: 18, + ), tooltip: 'Open in Maps', onPressed: () { NavigationHandoffService.showHandoffSheet( @@ -978,33 +1077,50 @@ class _RoutePlannerScreenState extends ConsumerState { ); }, padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 32), + constraints: const BoxConstraints( + minWidth: 32, + ), ), IconButton( - icon: const Icon(Icons.edit, - color: Colors.white60, size: 18), + icon: const Icon( + Icons.edit, + color: Colors.white60, + size: 18, + ), tooltip: 'Edit stop', onPressed: () => _showAddOrEditStopDialog( existingStop: stop, ), padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 32), + constraints: const BoxConstraints( + minWidth: 32, + ), ), IconButton( - icon: const Icon(Icons.delete_outline, - color: Color(0xFFFF5252), size: 18), + icon: const Icon( + Icons.delete_outline, + color: Color(0xFFFF5252), + size: 18, + ), tooltip: 'Remove stop', onPressed: () { ref - .read(routePlannerNotifierProvider.notifier) + .read( + routePlannerNotifierProvider.notifier, + ) .removeStop(stop.id); }, padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 32), + constraints: const BoxConstraints( + minWidth: 32, + ), ), const SizedBox(width: 4), - const Icon(Icons.drag_handle, - color: Colors.white30, size: 20), + const Icon( + Icons.drag_handle, + color: Colors.white30, + size: 20, + ), ], ), ); diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index 62084f5..be1b6c4 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -89,6 +89,7 @@ class _SettingsScreenState extends ConsumerState { final user = authState.user; return InkWell( + key: const ValueKey('settings_account_card'), onTap: () => context.go('/settings/account'), child: Padding( padding: const EdgeInsets.all(16.0), diff --git a/lib/features/settings/widgets/current_plan_card.dart b/lib/features/settings/widgets/current_plan_card.dart index 932e752..769edbd 100644 --- a/lib/features/settings/widgets/current_plan_card.dart +++ b/lib/features/settings/widgets/current_plan_card.dart @@ -174,6 +174,7 @@ class CurrentPlanCard extends ConsumerWidget { if (!isPro) ...[ Expanded( child: ElevatedButton( + key: const ValueKey('settings_upgrade_to_pro_button'), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF00E5FF), foregroundColor: Colors.black, @@ -218,6 +219,7 @@ class CurrentPlanCard extends ConsumerWidget { const SizedBox(width: 10), ], OutlinedButton( + key: const ValueKey('settings_pro_details_button'), style: OutlinedButton.styleFrom( foregroundColor: Colors.white70, side: const BorderSide(color: Colors.white24), diff --git a/lib/main.dart b/lib/main.dart index 2b3e6d2..493c2c9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'dart:io' show Platform; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:marionette_flutter/marionette_flutter.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:app_links/app_links.dart'; @@ -11,7 +14,13 @@ import 'features/records/records_service.dart'; import 'services/sync/sync_observer.dart'; Future main() async { - WidgetsFlutterBinding.ensureInitialized(); + final isFlutterTest = + !kIsWeb && Platform.environment.containsKey('FLUTTER_TEST'); + if (kDebugMode && !isFlutterTest) { + MarionetteBinding.ensureInitialized(); + } else { + WidgetsFlutterBinding.ensureInitialized(); + } try { await Firebase.initializeApp( @@ -68,7 +77,9 @@ class _MotorcycleDashAppState extends ConsumerState { void _handleDeepLink(Uri uri) { 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 { appRouter.go('/oauth/callback?${uri.query}'); } catch (_) {} diff --git a/lib/services/routes/navigation_handoff.dart b/lib/services/routes/navigation_handoff.dart index faf7e09..f1af5ff 100644 --- a/lib/services/routes/navigation_handoff.dart +++ b/lib/services/routes/navigation_handoff.dart @@ -32,8 +32,11 @@ class NavigationHandoffService { required double longitude, String? displayName, }) { - final dest = '${latitude.toStringAsFixed(6)},${longitude.toStringAsFixed(6)}'; - return Uri.parse('https://www.google.com/maps/dir/?api=1&destination=$dest'); + final 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. @@ -51,7 +54,8 @@ class NavigationHandoffService { required double longitude, 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)'); } @@ -79,7 +83,11 @@ class NavigationHandoffService { children: [ Row( children: [ - const Icon(Icons.navigation, color: Color(0xFF00E5FF), size: 22), + const Icon( + Icons.navigation, + color: Color(0xFF00E5FF), + size: 22, + ), const SizedBox(width: 12), Expanded( child: Column( @@ -112,6 +120,7 @@ class NavigationHandoffService { ), const SizedBox(height: 20), ListTile( + key: const ValueKey('handoff_google_maps_tile'), leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( @@ -120,9 +129,22 @@ class NavigationHandoffService { ), child: const Icon(Icons.map, color: Colors.white), ), - title: const Text('Google Maps', 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), + title: const Text( + 'Google Maps', + 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 { Navigator.pop(sheetContext); final uri = buildGoogleMapsUri( @@ -131,23 +153,43 @@ class NavigationHandoffService { displayName: displayName, ); if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); + await launchUrl( + uri, + mode: LaunchMode.externalApplication, + ); } }, ), const Divider(color: Colors.white12, height: 1), ListTile( + key: const ValueKey('handoff_waze_tile'), leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white10, 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 { Navigator.pop(sheetContext); final uri = buildWazeUri( @@ -155,12 +197,16 @@ class NavigationHandoffService { longitude: longitude, ); if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); + await launchUrl( + uri, + mode: LaunchMode.externalApplication, + ); } }, ), const Divider(color: Colors.white12, height: 1), ListTile( + key: const ValueKey('handoff_system_map_tile'), leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( @@ -169,9 +215,22 @@ class NavigationHandoffService { ), child: const Icon(Icons.place, color: Colors.white70), ), - title: const Text('System Map', 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), + title: const Text( + 'System Map', + 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 { Navigator.pop(sheetContext); final uri = buildSystemGeoUri( @@ -180,7 +239,10 @@ class NavigationHandoffService { displayName: displayName, ); if (await canLaunchUrl(uri)) { - await launchUrl(uri, mode: LaunchMode.externalApplication); + await launchUrl( + uri, + mode: LaunchMode.externalApplication, + ); } }, ), @@ -209,10 +271,7 @@ class NavigationHandoffService { break; case NavigationApp.waze: - uri = buildWazeUri( - latitude: stop.latitude, - longitude: stop.longitude, - ); + uri = buildWazeUri(latitude: stop.latitude, longitude: stop.longitude); break; case NavigationApp.platformDefault: @@ -232,7 +291,10 @@ class NavigationHandoffService { 'https://www.google.com/maps/search/?api=1&query=${stop.latitude},${stop.longitude}', ); if (await canLaunchUrl(fallbackUri)) { - return await launchUrl(fallbackUri, mode: LaunchMode.externalApplication); + return await launchUrl( + fallbackUri, + mode: LaunchMode.externalApplication, + ); } return false; } catch (e) { @@ -262,8 +324,9 @@ class NavigationHandoffService { .map((s) => '${s.latitude},${s.longitude}') .join('|'); - final waypointsParam = - waypointsStr.isNotEmpty ? '&waypoints=$waypointsStr' : ''; + final waypointsParam = waypointsStr.isNotEmpty + ? '&waypoints=$waypointsStr' + : ''; final uri = Uri.parse( '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); } } catch (e) { - debugPrint('[NavigationHandoffService] Failed to launch multi-stop route: $e'); + debugPrint( + '[NavigationHandoffService] Failed to launch multi-stop route: $e', + ); } } diff --git a/pubspec.lock b/pubspec.lock index 252498f..41ef0e6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -925,6 +925,14 @@ packages: url: "https://pub.dev" source: hosted 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: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2fb152b..b81616b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,6 +65,7 @@ dependencies: cloud_firestore: ^5.6.5 flutter_secure_storage: ^10.2.1 in_app_purchase: 3.3.0 + marionette_flutter: ^0.6.0 dev_dependencies: flutter_test: