chore: add debug-only Marionette test bridge

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

View File

@ -33,13 +33,15 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
@override
Widget build(BuildContext context) {
ref.listen<RideSession>(rideStateMachineProvider, (previous, next) {
if (previous?.state != RideState.riding && next.state == RideState.riding) {
if (previous?.state != RideState.riding &&
next.state == RideState.riding) {
if (widget.navigationShell.currentIndex != 0) {
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<MainLayout> {
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<MainLayout> {
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<MainLayout> {
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<MainLayout> {
},
),
),
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<MainLayout> {
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<MainLayout> {
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',
),
],
),

View File

@ -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,

File diff suppressed because it is too large Load Diff

View File

@ -72,6 +72,7 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
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<GroupRidesHubScreen>
),
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<GroupRidesHubScreen>
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<GroupRidesHubScreen>
}
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<GroupRidesHubScreen>
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<GroupRidesHubScreen>
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<GroupRidesHubScreen>
}
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<GroupRidesHubScreen>
),
),
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<GroupRidesHubScreen>
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<GroupRidesHubScreen>
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<GroupRidesHubScreen>
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<GroupRidesHubScreen>
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<GroupRidesHubScreen>
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<GroupRidesHubScreen>
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(

View File

@ -49,14 +49,18 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
@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<LiveGroupRideScreen>
);
}
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
),
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
// 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<LiveGroupRideScreen>
// 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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
: 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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
),
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<LiveGroupRideScreen>
),
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<LiveGroupRideScreen>
}
LiveGroupPresence? _findPresence(
List<LiveGroupPresence> presences, String uid) {
List<LiveGroupPresence> presences,
String uid,
) {
try {
return presences.firstWhere((p) => p.userId == uid);
} catch (_) {
@ -1044,7 +1120,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
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<LiveGroupRideScreen>
}
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<LiveGroupRideScreen>
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(

View File

@ -13,7 +13,8 @@ class HistoryScreen extends ConsumerStatefulWidget {
}
class _HistoryScreenState extends ConsumerState<HistoryScreen> {
int? _selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID
int?
_selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID
@override
Widget build(BuildContext context) {
@ -42,9 +43,14 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
// 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<HistoryScreen> {
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<HistoryScreen> {
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<HistoryScreen> {
),
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<HistoryScreen> {
);
}
Widget _buildFilterBar(List<GarageMotorcycle> bikes, GarageMotorcycle? defaultBike) {
Widget _buildFilterBar(
List<GarageMotorcycle> bikes,
GarageMotorcycle? defaultBike,
) {
if (bikes.isEmpty) return const SizedBox();
return Padding(
@ -188,10 +245,12 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
value: -1,
child: Text('Current (${defaultBike.modelName})'),
),
...bikes.map((b) => DropdownMenuItem<int?>(
value: b.id,
child: Text('${b.year} ${b.makeName} ${b.modelName}'),
)),
...bikes.map(
(b) => DropdownMenuItem<int?>(
value: b.id,
child: Text('${b.year} ${b.makeName} ${b.modelName}'),
),
),
],
onChanged: (val) {
setState(() {
@ -212,7 +271,10 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
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),
),
],
);
}

View File

@ -29,7 +29,10 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
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<RouteDetailScreen> {
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<RouteDetailScreen> {
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<RouteDetailScreen> {
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<RouteDetailScreen> {
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<RouteDetailScreen> {
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<RouteDetailScreen> {
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<RouteDetailScreen> {
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<RouteDetailScreen> {
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<RouteDetailScreen> {
// 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<RouteDetailScreen> {
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<RouteDetailScreen> {
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<RouteDetailScreen> {
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<RouteDetailScreen> {
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<RouteDetailScreen> {
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<RouteDetailScreen> {
// 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(

View File

@ -108,7 +108,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
}) {
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<RoutePlannerScreen> {
: (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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
// 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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
)
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
),
),
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
),
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
// 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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
),
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<RoutePlannerScreen> {
),
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
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<RoutePlannerScreen> {
// Stop Details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
@ -966,8 +1062,11 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
// 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<RoutePlannerScreen> {
);
},
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,
),
],
),
);

View File

@ -89,6 +89,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
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),

View File

@ -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),

View File

@ -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<void> 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<MotorcycleDashApp> {
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 (_) {}

View File

@ -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',
);
}
}

View File

@ -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:

View File

@ -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: