chore: add debug-only Marionette test bridge
This commit is contained in:
parent
23e63350b0
commit
cbeccb6063
|
|
@ -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',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 (_) {}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue