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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
ref.listen<RideSession>(rideStateMachineProvider, (previous, next) {
|
ref.listen<RideSession>(rideStateMachineProvider, (previous, next) {
|
||||||
if (previous?.state != RideState.riding && next.state == RideState.riding) {
|
if (previous?.state != RideState.riding &&
|
||||||
|
next.state == RideState.riding) {
|
||||||
if (widget.navigationShell.currentIndex != 0) {
|
if (widget.navigationShell.currentIndex != 0) {
|
||||||
widget.navigationShell.goBranch(0);
|
widget.navigationShell.goBranch(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape;
|
final isLandscape =
|
||||||
|
MediaQuery.of(context).orientation == Orientation.landscape;
|
||||||
|
|
||||||
if (isLandscape) {
|
if (isLandscape) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
@ -52,7 +54,9 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
physics: const ClampingScrollPhysics(),
|
physics: const ClampingScrollPhysics(),
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
child: IntrinsicHeight(
|
child: IntrinsicHeight(
|
||||||
child: NavigationRail(
|
child: NavigationRail(
|
||||||
minWidth: 54,
|
minWidth: 54,
|
||||||
|
|
@ -61,13 +65,22 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
|
||||||
onDestinationSelected: (index) {
|
onDestinationSelected: (index) {
|
||||||
widget.navigationShell.goBranch(
|
widget.navigationShell.goBranch(
|
||||||
index,
|
index,
|
||||||
initialLocation: index == widget.navigationShell.currentIndex,
|
initialLocation:
|
||||||
|
index == widget.navigationShell.currentIndex,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
backgroundColor: const Color(0xFF0A0A0A),
|
backgroundColor: const Color(0xFF0A0A0A),
|
||||||
indicatorColor: const Color(0xFF00E5FF).withValues(alpha: 0.18),
|
indicatorColor: const Color(
|
||||||
selectedIconTheme: const IconThemeData(color: Color(0xFF00E5FF), size: 20),
|
0xFF00E5FF,
|
||||||
unselectedIconTheme: const IconThemeData(color: Colors.white54, size: 18),
|
).withValues(alpha: 0.18),
|
||||||
|
selectedIconTheme: const IconThemeData(
|
||||||
|
color: Color(0xFF00E5FF),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
unselectedIconTheme: const IconThemeData(
|
||||||
|
color: Colors.white54,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
selectedLabelTextStyle: const TextStyle(
|
selectedLabelTextStyle: const TextStyle(
|
||||||
fontFamily: 'Inter',
|
fontFamily: 'Inter',
|
||||||
color: Color(0xFF00E5FF),
|
color: Color(0xFF00E5FF),
|
||||||
|
|
@ -84,32 +97,50 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
|
||||||
destinations: const [
|
destinations: const [
|
||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
padding: EdgeInsets.symmetric(vertical: 2),
|
padding: EdgeInsets.symmetric(vertical: 2),
|
||||||
icon: Icon(Icons.speed),
|
icon: Icon(
|
||||||
|
Icons.speed,
|
||||||
|
key: ValueKey('rail_dash_icon'),
|
||||||
|
),
|
||||||
label: Text('Dash'),
|
label: Text('Dash'),
|
||||||
),
|
),
|
||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
padding: EdgeInsets.symmetric(vertical: 2),
|
padding: EdgeInsets.symmetric(vertical: 2),
|
||||||
icon: Icon(Icons.map_outlined),
|
icon: Icon(
|
||||||
|
Icons.map_outlined,
|
||||||
|
key: ValueKey('rail_map_icon'),
|
||||||
|
),
|
||||||
label: Text('Map'),
|
label: Text('Map'),
|
||||||
),
|
),
|
||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
padding: EdgeInsets.symmetric(vertical: 2),
|
padding: EdgeInsets.symmetric(vertical: 2),
|
||||||
icon: Icon(Icons.history),
|
icon: Icon(
|
||||||
|
Icons.history,
|
||||||
|
key: ValueKey('rail_history_icon'),
|
||||||
|
),
|
||||||
label: Text('History'),
|
label: Text('History'),
|
||||||
),
|
),
|
||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
padding: EdgeInsets.symmetric(vertical: 2),
|
padding: EdgeInsets.symmetric(vertical: 2),
|
||||||
icon: Icon(Icons.emoji_events_outlined),
|
icon: Icon(
|
||||||
|
Icons.emoji_events_outlined,
|
||||||
|
key: ValueKey('rail_records_icon'),
|
||||||
|
),
|
||||||
label: Text('Records'),
|
label: Text('Records'),
|
||||||
),
|
),
|
||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
padding: EdgeInsets.symmetric(vertical: 2),
|
padding: EdgeInsets.symmetric(vertical: 2),
|
||||||
icon: Icon(Icons.two_wheeler),
|
icon: Icon(
|
||||||
|
Icons.two_wheeler,
|
||||||
|
key: ValueKey('rail_garage_icon'),
|
||||||
|
),
|
||||||
label: Text('Garage'),
|
label: Text('Garage'),
|
||||||
),
|
),
|
||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
padding: EdgeInsets.symmetric(vertical: 2),
|
padding: EdgeInsets.symmetric(vertical: 2),
|
||||||
icon: Icon(Icons.settings_outlined),
|
icon: Icon(
|
||||||
|
Icons.settings_outlined,
|
||||||
|
key: ValueKey('rail_settings_icon'),
|
||||||
|
),
|
||||||
label: Text('Settings'),
|
label: Text('Settings'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -120,7 +151,11 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const VerticalDivider(width: 1, thickness: 1, color: Color(0xFF1F1F1F)),
|
const VerticalDivider(
|
||||||
|
width: 1,
|
||||||
|
thickness: 1,
|
||||||
|
color: Color(0xFF1F1F1F),
|
||||||
|
),
|
||||||
Expanded(child: widget.navigationShell),
|
Expanded(child: widget.navigationShell),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -130,6 +165,7 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: widget.navigationShell,
|
body: widget.navigationShell,
|
||||||
bottomNavigationBar: BottomNavigationBar(
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
|
key: const ValueKey('main_bottom_nav_bar'),
|
||||||
currentIndex: widget.navigationShell.currentIndex,
|
currentIndex: widget.navigationShell.currentIndex,
|
||||||
onTap: (index) {
|
onTap: (index) {
|
||||||
widget.navigationShell.goBranch(
|
widget.navigationShell.goBranch(
|
||||||
|
|
@ -143,28 +179,34 @@ class _MainLayoutState extends ConsumerState<MainLayout> {
|
||||||
type: BottomNavigationBarType.fixed,
|
type: BottomNavigationBarType.fixed,
|
||||||
items: const [
|
items: const [
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.speed),
|
icon: Icon(Icons.speed, key: ValueKey('nav_dash_icon')),
|
||||||
label: 'Dash',
|
label: 'Dash',
|
||||||
|
tooltip: 'Dash',
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.map),
|
icon: Icon(Icons.map, key: ValueKey('nav_map_icon')),
|
||||||
label: 'Map',
|
label: 'Map',
|
||||||
|
tooltip: 'Map',
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.history),
|
icon: Icon(Icons.history, key: ValueKey('nav_history_icon')),
|
||||||
label: 'History',
|
label: 'History',
|
||||||
|
tooltip: 'History',
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.emoji_events),
|
icon: Icon(Icons.emoji_events, key: ValueKey('nav_records_icon')),
|
||||||
label: 'Records',
|
label: 'Records',
|
||||||
|
tooltip: 'Records',
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.two_wheeler),
|
icon: Icon(Icons.two_wheeler, key: ValueKey('nav_garage_icon')),
|
||||||
label: 'Garage',
|
label: 'Garage',
|
||||||
|
tooltip: 'Garage',
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.settings),
|
icon: Icon(Icons.settings, key: ValueKey('nav_settings_icon')),
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
|
tooltip: 'Settings',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ class GarageScreen extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
|
key: const ValueKey('garage_add_motorcycle_appbar_button'),
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.add_circle_outline,
|
Icons.add_circle_outline,
|
||||||
color: Color(0xFF00E5FF),
|
color: Color(0xFF00E5FF),
|
||||||
|
|
@ -71,6 +72,7 @@ class GarageScreen extends ConsumerWidget {
|
||||||
floatingActionButton:
|
floatingActionButton:
|
||||||
bikesAsync.value != null && bikesAsync.value!.isNotEmpty
|
bikesAsync.value != null && bikesAsync.value!.isNotEmpty
|
||||||
? FloatingActionButton.extended(
|
? FloatingActionButton.extended(
|
||||||
|
key: const ValueKey('garage_add_motorcycle_fab'),
|
||||||
backgroundColor: Colors.cyanAccent,
|
backgroundColor: Colors.cyanAccent,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
icon: const Icon(Icons.two_wheeler),
|
icon: const Icon(Icons.two_wheeler),
|
||||||
|
|
@ -130,6 +132,7 @@ class GarageScreen extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
|
key: const ValueKey('garage_add_motorcycle_empty_button'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.cyanAccent,
|
backgroundColor: Colors.cyanAccent,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -72,6 +72,7 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
actions: [
|
actions: [
|
||||||
if (isAuthed)
|
if (isAuthed)
|
||||||
IconButton(
|
IconButton(
|
||||||
|
key: const ValueKey('group_rides_create_appbar_button'),
|
||||||
icon: const Icon(Icons.add, color: Color(0xFF00E5FF)),
|
icon: const Icon(Icons.add, color: Color(0xFF00E5FF)),
|
||||||
tooltip: 'Create Group Ride',
|
tooltip: 'Create Group Ride',
|
||||||
onPressed: () => context.push('/group-rides/create'),
|
onPressed: () => context.push('/group-rides/create'),
|
||||||
|
|
@ -89,6 +90,7 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
),
|
),
|
||||||
floatingActionButton: isAuthed
|
floatingActionButton: isAuthed
|
||||||
? FloatingActionButton.extended(
|
? FloatingActionButton.extended(
|
||||||
|
key: const ValueKey('create_group_ride_fab'),
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
backgroundColor: const Color(0xFF00E5FF),
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
icon: const Icon(Icons.add_road),
|
icon: const Icon(Icons.add_road),
|
||||||
|
|
@ -134,15 +136,21 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
const Text(
|
const Text(
|
||||||
'To organize group rides, invite fellow riders, and coordinate meeting points, sign in with your permanent MotorSignal account.',
|
'To organize group rides, invite fellow riders, and coordinate meeting points, sign in with your permanent MotorSignal account.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 13, height: 1.4),
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 13,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
backgroundColor: const Color(0xFF00E5FF),
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(
|
||||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
|
horizontal: 24,
|
||||||
|
vertical: 14,
|
||||||
|
),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
|
|
@ -161,7 +169,10 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUpcomingTab(
|
Widget _buildUpcomingTab(
|
||||||
BuildContext context, WidgetRef ref, String currentUid) {
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
String currentUid,
|
||||||
|
) {
|
||||||
final ridesAsync = ref.watch(upcomingGroupRidesProvider);
|
final ridesAsync = ref.watch(upcomingGroupRidesProvider);
|
||||||
|
|
||||||
return ridesAsync.when(
|
return ridesAsync.when(
|
||||||
|
|
@ -190,20 +201,26 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
|
child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
|
||||||
),
|
),
|
||||||
error: (e, _) => Center(
|
error: (e, _) => Center(
|
||||||
child: Text('Error loading rides: $e',
|
child: Text(
|
||||||
style: const TextStyle(color: Colors.redAccent)),
|
'Error loading rides: $e',
|
||||||
|
style: const TextStyle(color: Colors.redAccent),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMyRidesTab(
|
Widget _buildMyRidesTab(
|
||||||
BuildContext context, WidgetRef ref, String currentUid) {
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
String currentUid,
|
||||||
|
) {
|
||||||
final ridesAsync = ref.watch(upcomingGroupRidesProvider);
|
final ridesAsync = ref.watch(upcomingGroupRidesProvider);
|
||||||
|
|
||||||
return ridesAsync.when(
|
return ridesAsync.when(
|
||||||
data: (rides) {
|
data: (rides) {
|
||||||
final myRides =
|
final myRides = rides
|
||||||
rides.where((r) => r.organizerUid == currentUid).toList();
|
.where((r) => r.organizerUid == currentUid)
|
||||||
|
.toList();
|
||||||
|
|
||||||
if (myRides.isEmpty) {
|
if (myRides.isEmpty) {
|
||||||
return _buildEmptyState(
|
return _buildEmptyState(
|
||||||
|
|
@ -229,8 +246,10 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
|
child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
|
||||||
),
|
),
|
||||||
error: (e, _) => Center(
|
error: (e, _) => Center(
|
||||||
child: Text('Error: $e',
|
child: Text(
|
||||||
style: const TextStyle(color: Colors.redAccent)),
|
'Error: $e',
|
||||||
|
style: const TextStyle(color: Colors.redAccent),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -283,7 +302,10 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRideCard(
|
Widget _buildRideCard(
|
||||||
BuildContext context, GroupRide ride, String currentUid) {
|
BuildContext context,
|
||||||
|
GroupRide ride,
|
||||||
|
String currentUid,
|
||||||
|
) {
|
||||||
final isOrganizer = ride.organizerUid == currentUid;
|
final isOrganizer = ride.organizerUid == currentUid;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
|
|
@ -298,6 +320,7 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
key: ValueKey('group_ride_card_${ride.id}'),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
onTap: () => context.push('/group-rides/${ride.id}'),
|
onTap: () => context.push('/group-rides/${ride.id}'),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|
@ -308,8 +331,10 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding:
|
padding: const EdgeInsets.symmetric(
|
||||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ride.isCanceled
|
color: ride.isCanceled
|
||||||
? Colors.redAccent.withValues(alpha: 0.2)
|
? Colors.redAccent.withValues(alpha: 0.2)
|
||||||
|
|
@ -331,7 +356,9 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
if (isOrganizer)
|
if (isOrganizer)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 6, vertical: 2),
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.amber.withValues(alpha: 0.2),
|
color: Colors.amber.withValues(alpha: 0.2),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
|
@ -348,8 +375,11 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.people_outline,
|
const Icon(
|
||||||
size: 16, color: Colors.white60),
|
Icons.people_outline,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white60,
|
||||||
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'${ride.participantCount}${ride.participantLimit != null ? '/${ride.participantLimit}' : ''}',
|
'${ride.participantCount}${ride.participantLimit != null ? '/${ride.participantLimit}' : ''}',
|
||||||
|
|
@ -375,7 +405,11 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.schedule, size: 14, color: Color(0xFF00E5FF)),
|
const Icon(
|
||||||
|
Icons.schedule,
|
||||||
|
size: 14,
|
||||||
|
color: Color(0xFF00E5FF),
|
||||||
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'Meet: ${_formatDateTime(ride.scheduledStartAt)} • KSU: ${_formatTime(ride.departureAt)}',
|
'Meet: ${_formatDateTime(ride.scheduledStartAt)} • KSU: ${_formatTime(ride.departureAt)}',
|
||||||
|
|
@ -390,8 +424,11 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.place_outlined,
|
const Icon(
|
||||||
size: 14, color: Colors.white54),
|
Icons.place_outlined,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.white54,
|
||||||
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -409,8 +446,11 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.flag_outlined,
|
const Icon(
|
||||||
size: 14, color: Color(0xFFFFB300)),
|
Icons.flag_outlined,
|
||||||
|
size: 14,
|
||||||
|
color: Color(0xFFFFB300),
|
||||||
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,18 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final rideAsync = ref.watch(groupRideStreamProvider(widget.rideId));
|
final rideAsync = ref.watch(groupRideStreamProvider(widget.rideId));
|
||||||
final participantsAsync =
|
final participantsAsync = ref.watch(
|
||||||
ref.watch(groupRideParticipantsProvider(widget.rideId));
|
groupRideParticipantsProvider(widget.rideId),
|
||||||
final presencesAsync =
|
);
|
||||||
ref.watch(groupRideLivePresenceProvider(widget.rideId));
|
final presencesAsync = ref.watch(
|
||||||
final broadcastState =
|
groupRideLivePresenceProvider(widget.rideId),
|
||||||
ref.watch(liveBroadcastControllerProvider(widget.rideId));
|
);
|
||||||
final broadcastController =
|
final broadcastState = ref.watch(
|
||||||
ref.read(liveBroadcastControllerProvider(widget.rideId).notifier);
|
liveBroadcastControllerProvider(widget.rideId),
|
||||||
|
);
|
||||||
|
final broadcastController = ref.read(
|
||||||
|
liveBroadcastControllerProvider(widget.rideId).notifier,
|
||||||
|
);
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
final currentUid = user?.uid;
|
final currentUid = user?.uid;
|
||||||
|
|
||||||
|
|
@ -78,7 +82,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final isOrganizer = currentUid != null && ride.organizerUid == currentUid;
|
final isOrganizer =
|
||||||
|
currentUid != null && ride.organizerUid == currentUid;
|
||||||
|
|
||||||
// Check if ride was closed
|
// Check if ride was closed
|
||||||
if (ride.isCompleted || ride.isCanceled) {
|
if (ride.isCompleted || ride.isCanceled) {
|
||||||
|
|
@ -103,7 +108,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
ride.isCompleted ? Icons.flag : Icons.cancel,
|
ride.isCompleted ? Icons.flag : Icons.cancel,
|
||||||
color: ride.isCompleted ? Colors.greenAccent : Colors.redAccent,
|
color: ride.isCompleted
|
||||||
|
? Colors.greenAccent
|
||||||
|
: Colors.redAccent,
|
||||||
size: 48,
|
size: 48,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -136,28 +143,32 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
|
|
||||||
return participantsAsync.when(
|
return participantsAsync.when(
|
||||||
data: (participants) {
|
data: (participants) {
|
||||||
final acceptedParticipants =
|
final acceptedParticipants = participants
|
||||||
participants.where((p) => p.status == ParticipantStatus.accepted).toList();
|
.where((p) => p.status == ParticipantStatus.accepted)
|
||||||
|
.toList();
|
||||||
|
|
||||||
return presencesAsync.when(
|
return presencesAsync.when(
|
||||||
data: (presences) {
|
data: (presences) {
|
||||||
// Determine Ride Lead (first participant with lead role, or organizer)
|
// Determine Ride Lead (first participant with lead role, or organizer)
|
||||||
GroupRideParticipant? leadParticipant;
|
GroupRideParticipant? leadParticipant;
|
||||||
try {
|
try {
|
||||||
leadParticipant = acceptedParticipants
|
leadParticipant = acceptedParticipants.firstWhere(
|
||||||
.firstWhere((p) => p.role == GroupRideRole.lead);
|
(p) => p.role == GroupRideRole.lead,
|
||||||
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
try {
|
try {
|
||||||
leadParticipant = acceptedParticipants
|
leadParticipant = acceptedParticipants.firstWhere(
|
||||||
.firstWhere((p) => p.role == GroupRideRole.organizer);
|
(p) => p.role == GroupRideRole.organizer,
|
||||||
|
);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
LiveGroupPresence? leadPresence;
|
LiveGroupPresence? leadPresence;
|
||||||
if (leadParticipant != null) {
|
if (leadParticipant != null) {
|
||||||
try {
|
try {
|
||||||
leadPresence = presences
|
leadPresence = presences.firstWhere(
|
||||||
.firstWhere((p) => p.userId == leadParticipant!.userId);
|
(p) => p.userId == leadParticipant!.userId,
|
||||||
|
);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,7 +176,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
LiveGroupPresence? regroupCaller;
|
LiveGroupPresence? regroupCaller;
|
||||||
try {
|
try {
|
||||||
regroupCaller = presences.firstWhere(
|
regroupCaller = presences.firstWhere(
|
||||||
(p) => p.status == LivePresenceStatus.regrouping && p.isFresh,
|
(p) =>
|
||||||
|
p.status == LivePresenceStatus.regrouping && p.isFresh,
|
||||||
);
|
);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
|
|
@ -246,7 +258,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
context: context,
|
context: context,
|
||||||
caller: regroupCaller,
|
caller: regroupCaller,
|
||||||
participants: acceptedParticipants,
|
participants: acceptedParticipants,
|
||||||
isLeadOrOrganizer: isOrganizer ||
|
isLeadOrOrganizer:
|
||||||
|
isOrganizer ||
|
||||||
(leadParticipant != null &&
|
(leadParticipant != null &&
|
||||||
leadParticipant.userId == currentUid),
|
leadParticipant.userId == currentUid),
|
||||||
controller: broadcastController,
|
controller: broadcastController,
|
||||||
|
|
@ -304,8 +317,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
error: (e, _) => Scaffold(
|
error: (e, _) => Scaffold(
|
||||||
backgroundColor: const Color(0xFF0D0E11),
|
backgroundColor: const Color(0xFF0D0E11),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Text('Error loading live presence: $e',
|
child: Text(
|
||||||
style: const TextStyle(color: Colors.redAccent)),
|
'Error loading live presence: $e',
|
||||||
|
style: const TextStyle(color: Colors.redAccent),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -319,8 +334,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
error: (e, _) => Scaffold(
|
error: (e, _) => Scaffold(
|
||||||
backgroundColor: const Color(0xFF0D0E11),
|
backgroundColor: const Color(0xFF0D0E11),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Text('Error loading participants: $e',
|
child: Text(
|
||||||
style: const TextStyle(color: Colors.redAccent)),
|
'Error loading participants: $e',
|
||||||
|
style: const TextStyle(color: Colors.redAccent),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -334,8 +351,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
error: (e, _) => Scaffold(
|
error: (e, _) => Scaffold(
|
||||||
backgroundColor: const Color(0xFF0D0E11),
|
backgroundColor: const Color(0xFF0D0E11),
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Text('Error loading ride: $e',
|
child: Text(
|
||||||
style: const TextStyle(color: Colors.redAccent)),
|
'Error loading ride: $e',
|
||||||
|
style: const TextStyle(color: Colors.redAccent),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -406,7 +425,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
actions: [
|
actions: [
|
||||||
if (isOrganizer)
|
if (isOrganizer)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.stop_circle_outlined, color: Colors.redAccent),
|
icon: const Icon(
|
||||||
|
Icons.stop_circle_outlined,
|
||||||
|
color: Colors.redAccent,
|
||||||
|
),
|
||||||
tooltip: 'End Ride',
|
tooltip: 'End Ride',
|
||||||
onPressed: () => _confirmEndRide(context, ride, currentUid),
|
onPressed: () => _confirmEndRide(context, ride, currentUid),
|
||||||
),
|
),
|
||||||
|
|
@ -435,7 +457,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
String callerName = caller.userId;
|
String callerName = caller.userId;
|
||||||
try {
|
try {
|
||||||
final p = participants.firstWhere((p) => p.userId == caller.userId);
|
final p = participants.firstWhere((p) => p.userId == caller.userId);
|
||||||
callerName = p.riderDisplayName.isNotEmpty ? p.riderDisplayName : p.displayHandle;
|
callerName = p.riderDisplayName.isNotEmpty
|
||||||
|
? p.riderDisplayName
|
||||||
|
: p.displayHandle;
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
|
@ -450,7 +474,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.warning_amber_rounded, color: Colors.black, size: 22),
|
const Icon(
|
||||||
|
Icons.warning_amber_rounded,
|
||||||
|
color: Colors.black,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -484,7 +512,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
minimumSize: Size.zero,
|
minimumSize: Size.zero,
|
||||||
),
|
),
|
||||||
onPressed: () => controller.clearRegroup(),
|
onPressed: () => controller.clearRegroup(),
|
||||||
child: const Text('CLEAR', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold)),
|
child: const Text(
|
||||||
|
'CLEAR',
|
||||||
|
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
TextButton(
|
TextButton(
|
||||||
|
|
@ -495,7 +526,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
minimumSize: Size.zero,
|
minimumSize: Size.zero,
|
||||||
),
|
),
|
||||||
onPressed: () => controller.triggerRegroup(),
|
onPressed: () => controller.triggerRegroup(),
|
||||||
child: const Text('ACK', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold)),
|
child: const Text(
|
||||||
|
'ACK',
|
||||||
|
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -530,9 +564,13 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
minimumSize: Size.zero,
|
minimumSize: Size.zero,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(groupRideLiveSharingAllowedProvider.notifier).state = true;
|
ref.read(groupRideLiveSharingAllowedProvider.notifier).state =
|
||||||
|
true;
|
||||||
},
|
},
|
||||||
child: const Text('ENABLE', style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold)),
|
child: const Text(
|
||||||
|
'ENABLE',
|
||||||
|
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -573,9 +611,12 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
final isSelected = currentStatus == status;
|
final isSelected = currentStatus == status;
|
||||||
|
|
||||||
Color activeColor = const Color(0xFF00E5FF);
|
Color activeColor = const Color(0xFF00E5FF);
|
||||||
if (status == LivePresenceStatus.needFuel) activeColor = Colors.orangeAccent;
|
if (status == LivePresenceStatus.needFuel)
|
||||||
if (status == LivePresenceStatus.regrouping) activeColor = Colors.amber;
|
activeColor = Colors.orangeAccent;
|
||||||
if (status == LivePresenceStatus.arrived) activeColor = Colors.greenAccent;
|
if (status == LivePresenceStatus.regrouping)
|
||||||
|
activeColor = Colors.amber;
|
||||||
|
if (status == LivePresenceStatus.arrived)
|
||||||
|
activeColor = Colors.greenAccent;
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
|
@ -605,7 +646,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isSelected ? activeColor : Colors.white70,
|
color: isSelected ? activeColor : Colors.white70,
|
||||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.w500,
|
fontWeight: isSelected
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.w500,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -662,17 +705,17 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
Text(
|
Text(
|
||||||
broadcastState.isBroadcasting
|
broadcastState.isBroadcasting
|
||||||
? (broadcastState.isPaused
|
? (broadcastState.isPaused
|
||||||
? 'SHARING PAUSED'
|
? 'SHARING PAUSED'
|
||||||
: (broadcastState.isBackground
|
: (broadcastState.isBackground
|
||||||
? 'BACKGROUND SHARING'
|
? 'BACKGROUND SHARING'
|
||||||
: 'SHARING LIVE'))
|
: 'SHARING LIVE'))
|
||||||
: 'GHOST MODE',
|
: 'GHOST MODE',
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: broadcastState.isBroadcasting
|
color: broadcastState.isBroadcasting
|
||||||
? (broadcastState.isPaused
|
? (broadcastState.isPaused
|
||||||
? Colors.amber
|
? Colors.amber
|
||||||
: const Color(0xFF00E5FF))
|
: const Color(0xFF00E5FF))
|
||||||
: Colors.white60,
|
: Colors.white60,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
|
|
@ -682,18 +725,22 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
Text(
|
Text(
|
||||||
broadcastState.isBroadcasting
|
broadcastState.isBroadcasting
|
||||||
? (broadcastState.isPaused
|
? (broadcastState.isPaused
|
||||||
? 'Reconnecting (${broadcastState.retryCount})...'
|
? 'Reconnecting (${broadcastState.retryCount})...'
|
||||||
: (broadcastState.isBackground
|
: (broadcastState.isBackground
|
||||||
? 'Active via foreground service'
|
? 'Active via foreground service'
|
||||||
: 'Broadcasting to riders'))
|
: 'Broadcasting to riders'))
|
||||||
: 'Location paused',
|
: 'Location paused',
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(color: Colors.white38, fontSize: 10),
|
style: const TextStyle(
|
||||||
|
color: Colors.white38,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Switch(
|
Switch(
|
||||||
|
key: const ValueKey('live_ride_broadcast_switch'),
|
||||||
value: broadcastState.isOptedIn,
|
value: broadcastState.isOptedIn,
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
activeThumbColor: const Color(0xFF00E5FF),
|
activeThumbColor: const Color(0xFF00E5FF),
|
||||||
|
|
@ -707,12 +754,17 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
|
|
||||||
// Recenter Map Button
|
// Recenter Map Button
|
||||||
IconButton(
|
IconButton(
|
||||||
|
key: const ValueKey('live_ride_recenter_button'),
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.white10,
|
backgroundColor: Colors.white10,
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
minimumSize: const Size(36, 36),
|
minimumSize: const Size(36, 36),
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.my_location, color: Colors.white70, size: 18),
|
icon: const Icon(
|
||||||
|
Icons.my_location,
|
||||||
|
color: Colors.white70,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
tooltip: 'Center on Me',
|
tooltip: 'Center on Me',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (broadcastState.lastPosition != null) {
|
if (broadcastState.lastPosition != null) {
|
||||||
|
|
@ -738,6 +790,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
|
|
||||||
// Roster Opener Button
|
// Roster Opener Button
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
|
key: const ValueKey('live_ride_roster_button'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15),
|
backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15),
|
||||||
foregroundColor: const Color(0xFF00E5FF),
|
foregroundColor: const Color(0xFF00E5FF),
|
||||||
|
|
@ -877,7 +930,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
participant.riderDisplayName.isNotEmpty
|
participant.riderDisplayName.isNotEmpty
|
||||||
? participant.riderDisplayName[0].toUpperCase()
|
? participant.riderDisplayName[0].toUpperCase()
|
||||||
: '?',
|
: '?',
|
||||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isLive)
|
if (isLive)
|
||||||
|
|
@ -908,7 +964,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
: participant.displayHandle,
|
: participant.displayHandle,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isCaller ? const Color(0xFF00E5FF) : Colors.white,
|
color: isCaller
|
||||||
|
? const Color(0xFF00E5FF)
|
||||||
|
: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
|
|
@ -917,9 +975,14 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
if (participant.role != GroupRideRole.participant) ...[
|
if (participant.role != GroupRideRole.participant) ...[
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 1,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _roleColor(participant.role).withValues(alpha: 0.2),
|
color: _roleColor(
|
||||||
|
participant.role,
|
||||||
|
).withValues(alpha: 0.2),
|
||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(3),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -954,10 +1017,16 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
),
|
),
|
||||||
|
|
||||||
if (distanceString.isNotEmpty) ...[
|
if (distanceString.isNotEmpty) ...[
|
||||||
const Text(' • ', style: TextStyle(color: Colors.white24, fontSize: 10)),
|
const Text(
|
||||||
|
' • ',
|
||||||
|
style: TextStyle(color: Colors.white24, fontSize: 10),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
distanceString,
|
distanceString,
|
||||||
style: const TextStyle(color: Colors.white60, fontSize: 10),
|
style: const TextStyle(
|
||||||
|
color: Colors.white60,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
@ -980,11 +1049,16 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.gps_fixed, size: 16, color: Color(0xFF00E5FF)),
|
icon: const Icon(
|
||||||
|
Icons.gps_fixed,
|
||||||
|
size: 16,
|
||||||
|
color: Color(0xFF00E5FF),
|
||||||
|
),
|
||||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
tooltip: 'Center on rider',
|
tooltip: 'Center on rider',
|
||||||
onPressed: () => onCenter(AppLatLng(presence.latitude, presence.longitude)),
|
onPressed: () =>
|
||||||
|
onCenter(AppLatLng(presence.latitude, presence.longitude)),
|
||||||
),
|
),
|
||||||
] else
|
] else
|
||||||
const Text(
|
const Text(
|
||||||
|
|
@ -1029,7 +1103,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
}
|
}
|
||||||
|
|
||||||
LiveGroupPresence? _findPresence(
|
LiveGroupPresence? _findPresence(
|
||||||
List<LiveGroupPresence> presences, String uid) {
|
List<LiveGroupPresence> presences,
|
||||||
|
String uid,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
return presences.firstWhere((p) => p.userId == uid);
|
return presences.firstWhere((p) => p.userId == uid);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
|
|
@ -1044,7 +1120,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
required GroupRideLocation meetup,
|
required GroupRideLocation meetup,
|
||||||
}) {
|
}) {
|
||||||
if (customCenter != null) return customCenter;
|
if (customCenter != null) return customCenter;
|
||||||
if (localPos != null) return AppLatLng(localPos.latitude, localPos.longitude);
|
if (localPos != null)
|
||||||
|
return AppLatLng(localPos.latitude, localPos.longitude);
|
||||||
if (leadPresence != null) {
|
if (leadPresence != null) {
|
||||||
return AppLatLng(leadPresence.latitude, leadPresence.longitude);
|
return AppLatLng(leadPresence.latitude, leadPresence.longitude);
|
||||||
}
|
}
|
||||||
|
|
@ -1066,7 +1143,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
markers.add(
|
markers.add(
|
||||||
AppMapMarker(
|
AppMapMarker(
|
||||||
id: 'meetup_point',
|
id: 'meetup_point',
|
||||||
position: AppLatLng(ride.meetingPoint.latitude, ride.meetingPoint.longitude),
|
position: AppLatLng(
|
||||||
|
ride.meetingPoint.latitude,
|
||||||
|
ride.meetingPoint.longitude,
|
||||||
|
),
|
||||||
label: ride.meetingPoint.displayName.isNotEmpty
|
label: ride.meetingPoint.displayName.isNotEmpty
|
||||||
? ride.meetingPoint.displayName
|
? ride.meetingPoint.displayName
|
||||||
: 'Meetup',
|
: 'Meetup',
|
||||||
|
|
@ -1076,7 +1156,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
color: Colors.greenAccent,
|
color: Colors.greenAccent,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)),
|
BoxShadow(
|
||||||
|
color: Colors.black45,
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.flag, color: Colors.black, size: 16),
|
child: const Icon(Icons.flag, color: Colors.black, size: 16),
|
||||||
|
|
@ -1106,7 +1190,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
color: Color(0xFF00E5FF),
|
color: Color(0xFF00E5FF),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)),
|
BoxShadow(
|
||||||
|
color: Colors.black45,
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -1136,14 +1224,20 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
AppMapMarker(
|
AppMapMarker(
|
||||||
id: 'stop_$i',
|
id: 'stop_$i',
|
||||||
position: AppLatLng(stop.latitude, stop.longitude),
|
position: AppLatLng(stop.latitude, stop.longitude),
|
||||||
label: stop.displayName.isNotEmpty ? stop.displayName : 'Stop ${i + 1}',
|
label: stop.displayName.isNotEmpty
|
||||||
|
? stop.displayName
|
||||||
|
: 'Stop ${i + 1}',
|
||||||
iconWidget: Container(
|
iconWidget: Container(
|
||||||
padding: const EdgeInsets.all(5),
|
padding: const EdgeInsets.all(5),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.blueAccent,
|
color: Colors.blueAccent,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)),
|
BoxShadow(
|
||||||
|
color: Colors.black45,
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -1173,7 +1267,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
markers.add(
|
markers.add(
|
||||||
AppMapMarker(
|
AppMapMarker(
|
||||||
id: 'destination_point',
|
id: 'destination_point',
|
||||||
position: AppLatLng(ride.destination!.latitude, ride.destination!.longitude),
|
position: AppLatLng(
|
||||||
|
ride.destination!.latitude,
|
||||||
|
ride.destination!.longitude,
|
||||||
|
),
|
||||||
label: ride.destination!.displayName.isNotEmpty
|
label: ride.destination!.displayName.isNotEmpty
|
||||||
? ride.destination!.displayName
|
? ride.destination!.displayName
|
||||||
: 'Destination',
|
: 'Destination',
|
||||||
|
|
@ -1183,10 +1280,18 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
color: Colors.redAccent,
|
color: Colors.redAccent,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)),
|
BoxShadow(
|
||||||
|
color: Colors.black45,
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.sports_score, color: Colors.white, size: 16),
|
child: const Icon(
|
||||||
|
Icons.sports_score,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -1199,7 +1304,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
p = participants.firstWhere((item) => item.userId == presence.userId);
|
p = participants.firstWhere((item) => item.userId == presence.userId);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
final isLead = presence.userId == leadUid || p?.role == GroupRideRole.lead;
|
final isLead =
|
||||||
|
presence.userId == leadUid || p?.role == GroupRideRole.lead;
|
||||||
final isSweep = p?.role == GroupRideRole.sweep;
|
final isSweep = p?.role == GroupRideRole.sweep;
|
||||||
final isCaller = presence.userId == currentUid;
|
final isCaller = presence.userId == currentUid;
|
||||||
|
|
||||||
|
|
@ -1207,8 +1313,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
Color markerColor = const Color(0xFF00E5FF);
|
Color markerColor = const Color(0xFF00E5FF);
|
||||||
if (isLead) markerColor = const Color(0xFFFFD700); // Gold for lead
|
if (isLead) markerColor = const Color(0xFFFFD700); // Gold for lead
|
||||||
if (isSweep) markerColor = Colors.orangeAccent; // Orange for sweep
|
if (isSweep) markerColor = Colors.orangeAccent; // Orange for sweep
|
||||||
if (presence.status == LivePresenceStatus.regrouping) markerColor = Colors.amber;
|
if (presence.status == LivePresenceStatus.regrouping)
|
||||||
if (presence.status == LivePresenceStatus.needFuel) markerColor = Colors.deepOrangeAccent;
|
markerColor = Colors.amber;
|
||||||
|
if (presence.status == LivePresenceStatus.needFuel)
|
||||||
|
markerColor = Colors.deepOrangeAccent;
|
||||||
|
|
||||||
final isStale = presence.isStale;
|
final isStale = presence.isStale;
|
||||||
final opacity = isStale ? 0.4 : 1.0;
|
final opacity = isStale ? 0.4 : 1.0;
|
||||||
|
|
@ -1256,14 +1364,19 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _confirmEndRide(
|
void _confirmEndRide(
|
||||||
BuildContext context, GroupRide ride, String? currentUid) {
|
BuildContext context,
|
||||||
|
GroupRide ride,
|
||||||
|
String? currentUid,
|
||||||
|
) {
|
||||||
if (currentUid == null) return;
|
if (currentUid == null) return;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
backgroundColor: const Color(0xFF161922),
|
backgroundColor: const Color(0xFF161922),
|
||||||
title: const Text('End Group Ride?',
|
title: const Text(
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
'End Group Ride?',
|
||||||
|
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
content: const Text(
|
content: const Text(
|
||||||
'This will conclude the group ride for all participants and terminate live location sharing.',
|
'This will conclude the group ride for all participants and terminate live location sharing.',
|
||||||
style: TextStyle(color: Colors.white70),
|
style: TextStyle(color: Colors.white70),
|
||||||
|
|
@ -1271,7 +1384,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx),
|
onPressed: () => Navigator.pop(ctx),
|
||||||
child: const Text('CANCEL', style: TextStyle(color: Colors.white60)),
|
child: const Text(
|
||||||
|
'CANCEL',
|
||||||
|
style: TextStyle(color: Colors.white60),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ class HistoryScreen extends ConsumerStatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
||||||
int? _selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID
|
int?
|
||||||
|
_selectedMotorcycleId; // null = All Motorcycles, -1 = Current, else specific ID
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -42,9 +43,14 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
||||||
|
|
||||||
// Filter logic
|
// Filter logic
|
||||||
if (_selectedMotorcycleId == -1 && defaultBike != null) {
|
if (_selectedMotorcycleId == -1 && defaultBike != null) {
|
||||||
rides = rides.where((r) => r.motorcycleId == defaultBike.id).toList();
|
rides = rides
|
||||||
} else if (_selectedMotorcycleId != null && _selectedMotorcycleId! > 0) {
|
.where((r) => r.motorcycleId == defaultBike.id)
|
||||||
rides = rides.where((r) => r.motorcycleId == _selectedMotorcycleId).toList();
|
.toList();
|
||||||
|
} else if (_selectedMotorcycleId != null &&
|
||||||
|
_selectedMotorcycleId! > 0) {
|
||||||
|
rides = rides
|
||||||
|
.where((r) => r.motorcycleId == _selectedMotorcycleId)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by start time descending
|
// Sort by start time descending
|
||||||
|
|
@ -64,14 +70,21 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
||||||
itemCount: rides.length,
|
itemCount: rides.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final ride = rides[index];
|
final ride = rides[index];
|
||||||
final bikeName = (ride.motorcycleYear != null && ride.motorcycleMake != null && ride.motorcycleModel != null)
|
final bikeName =
|
||||||
|
(ride.motorcycleYear != null &&
|
||||||
|
ride.motorcycleMake != null &&
|
||||||
|
ride.motorcycleModel != null)
|
||||||
? '${ride.motorcycleYear} ${ride.motorcycleMake} ${ride.motorcycleModel}'
|
? '${ride.motorcycleYear} ${ride.motorcycleMake} ${ride.motorcycleModel}'
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
horizontal: 4,
|
||||||
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
key: ValueKey('history_ride_card_${ride.id}'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
|
@ -92,23 +105,39 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Ride ${ride.id}',
|
'Ride ${ride.id}',
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (bikeName != null) ...[
|
if (bikeName != null) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.cyanAccent.withValues(alpha: 0.15),
|
color: Colors.cyanAccent
|
||||||
borderRadius: BorderRadius.circular(4),
|
.withValues(alpha: 0.15),
|
||||||
border: Border.all(color: Colors.cyanAccent.withValues(alpha: 0.5)),
|
borderRadius:
|
||||||
|
BorderRadius.circular(4),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.cyanAccent
|
||||||
|
.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
bikeName,
|
bikeName,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.cyanAccent),
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.cyanAccent,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -118,27 +147,52 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
ride.startTime.toLocal().toString().split('.')[0],
|
ride.startTime.toLocal().toString().split(
|
||||||
style: const TextStyle(color: Colors.grey, fontSize: 11),
|
'.',
|
||||||
|
)[0],
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
_buildStatCol('TRIP', '${ride.distanceMiles.toStringAsFixed(1)} mi'),
|
_buildStatCol(
|
||||||
_buildStatCol('TIME', '${(ride.movingTime / 60).toStringAsFixed(0)} min'),
|
'TRIP',
|
||||||
_buildStatCol('TOP SPD', '${ride.topSpeedMph.toStringAsFixed(0)} mph'),
|
'${ride.distanceMiles.toStringAsFixed(1)} mi',
|
||||||
|
),
|
||||||
|
_buildStatCol(
|
||||||
|
'TIME',
|
||||||
|
'${(ride.movingTime / 60).toStringAsFixed(0)} min',
|
||||||
|
),
|
||||||
|
_buildStatCol(
|
||||||
|
'TOP SPD',
|
||||||
|
'${ride.topSpeedMph.toStringAsFixed(0)} mph',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
_buildStatCol('MAX LEAN', '${math.max(ride.maxLeanLeft, ride.maxLeanRight).toStringAsFixed(0)}°'),
|
_buildStatCol(
|
||||||
_buildStatCol('HARD BRK', '${ride.maxBrakingG.toStringAsFixed(2)}G'),
|
'MAX LEAN',
|
||||||
_buildStatCol('ELEV GAIN', '${ride.elevationGain.toStringAsFixed(0)}m'),
|
'${math.max(ride.maxLeanLeft, ride.maxLeanRight).toStringAsFixed(0)}°',
|
||||||
|
),
|
||||||
|
_buildStatCol(
|
||||||
|
'HARD BRK',
|
||||||
|
'${ride.maxBrakingG.toStringAsFixed(2)}G',
|
||||||
|
),
|
||||||
|
_buildStatCol(
|
||||||
|
'ELEV GAIN',
|
||||||
|
'${ride.elevationGain.toStringAsFixed(0)}m',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -156,7 +210,10 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFilterBar(List<GarageMotorcycle> bikes, GarageMotorcycle? defaultBike) {
|
Widget _buildFilterBar(
|
||||||
|
List<GarageMotorcycle> bikes,
|
||||||
|
GarageMotorcycle? defaultBike,
|
||||||
|
) {
|
||||||
if (bikes.isEmpty) return const SizedBox();
|
if (bikes.isEmpty) return const SizedBox();
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
|
|
@ -188,10 +245,12 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
||||||
value: -1,
|
value: -1,
|
||||||
child: Text('Current (${defaultBike.modelName})'),
|
child: Text('Current (${defaultBike.modelName})'),
|
||||||
),
|
),
|
||||||
...bikes.map((b) => DropdownMenuItem<int?>(
|
...bikes.map(
|
||||||
value: b.id,
|
(b) => DropdownMenuItem<int?>(
|
||||||
child: Text('${b.year} ${b.makeName} ${b.modelName}'),
|
value: b.id,
|
||||||
)),
|
child: Text('${b.year} ${b.makeName} ${b.modelName}'),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -212,7 +271,10 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)),
|
Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)),
|
||||||
Text(val, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
Text(
|
||||||
|
val,
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,10 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
side: const BorderSide(color: Color(0xFF2A2E3D)),
|
side: const BorderSide(color: Color(0xFF2A2E3D)),
|
||||||
),
|
),
|
||||||
title: const Text('Delete Route', style: TextStyle(color: Colors.white)),
|
title: const Text(
|
||||||
|
'Delete Route',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
content: Text(
|
content: Text(
|
||||||
'Are you sure you want to delete "${route.title}"? This cannot be undone.',
|
'Are you sure you want to delete "${route.title}"? This cannot be undone.',
|
||||||
style: const TextStyle(color: Colors.white70),
|
style: const TextStyle(color: Colors.white70),
|
||||||
|
|
@ -37,7 +40,10 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx, false),
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
child: const Text('CANCEL', style: TextStyle(color: Colors.white54)),
|
child: const Text(
|
||||||
|
'CANCEL',
|
||||||
|
style: TextStyle(color: Colors.white54),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|
@ -58,17 +64,17 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
final service = ref.read(routeServiceProvider);
|
final service = ref.read(routeServiceProvider);
|
||||||
await service.deleteRoute(widget.routeId);
|
await service.deleteRoute(widget.routeId);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
const SnackBar(content: Text('Route deleted.')),
|
context,
|
||||||
);
|
).showSnackBar(const SnackBar(content: Text('Route deleted.')));
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _isDeleting = false);
|
setState(() => _isDeleting = false);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
SnackBar(content: Text('Failed to delete route: $e')),
|
context,
|
||||||
);
|
).showSnackBar(SnackBar(content: Text('Failed to delete route: $e')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -132,8 +138,11 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.error_outline,
|
const Icon(
|
||||||
size: 48, color: Color(0xFFFF5252)),
|
Icons.error_outline,
|
||||||
|
size: 48,
|
||||||
|
color: Color(0xFFFF5252),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
'Could not load route: $err',
|
'Could not load route: $err',
|
||||||
|
|
@ -160,7 +169,8 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
final markers = route.stops.map((stop) {
|
final markers = route.stops.map((stop) {
|
||||||
final isFirst = stop.orderIndex == 0;
|
final isFirst = stop.orderIndex == 0;
|
||||||
final isLast =
|
final isLast =
|
||||||
stop.orderIndex == route.stops.length - 1 && route.stops.length > 1;
|
stop.orderIndex == route.stops.length - 1 &&
|
||||||
|
route.stops.length > 1;
|
||||||
|
|
||||||
return AppMapMarker(
|
return AppMapMarker(
|
||||||
id: stop.id,
|
id: stop.id,
|
||||||
|
|
@ -172,12 +182,15 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
color: isFirst
|
color: isFirst
|
||||||
? const Color(0xFF00E676)
|
? const Color(0xFF00E676)
|
||||||
: isLast
|
: isLast
|
||||||
? const Color(0xFFFF5252)
|
? const Color(0xFFFF5252)
|
||||||
: const Color(0xFF00E5FF),
|
: const Color(0xFF00E5FF),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
boxShadow: const [
|
boxShadow: const [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black54, blurRadius: 4, spreadRadius: 1),
|
color: Colors.black54,
|
||||||
|
blurRadius: 4,
|
||||||
|
spreadRadius: 1,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -216,7 +229,9 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
|
|
||||||
final mapCenter = route.stops.isNotEmpty
|
final mapCenter = route.stops.isNotEmpty
|
||||||
? AppLatLng(
|
? AppLatLng(
|
||||||
route.stops.first.latitude, route.stops.first.longitude)
|
route.stops.first.latitude,
|
||||||
|
route.stops.first.longitude,
|
||||||
|
)
|
||||||
: const AppLatLng(37.7749, -122.4194);
|
: const AppLatLng(37.7749, -122.4194);
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
|
|
@ -240,9 +255,13 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
left: 8,
|
left: 8,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 10, vertical: 4),
|
horizontal: 10,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF13151A).withValues(alpha: 0.9),
|
color: const Color(
|
||||||
|
0xFF13151A,
|
||||||
|
).withValues(alpha: 0.9),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
border: Border.all(color: const Color(0xFF2A2E3D)),
|
border: Border.all(color: const Color(0xFF2A2E3D)),
|
||||||
),
|
),
|
||||||
|
|
@ -370,35 +389,54 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
|
key: const ValueKey('route_detail_edit_button'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.push('/routes/planner?routeId=${route.id}');
|
context.push(
|
||||||
|
'/routes/planner?routeId=${route.id}',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.edit,
|
icon: const Icon(
|
||||||
size: 16, color: Color(0xFF00E5FF)),
|
Icons.edit,
|
||||||
label: const Text('EDIT ROUTE',
|
size: 16,
|
||||||
style: TextStyle(color: Color(0xFF00E5FF))),
|
color: Color(0xFF00E5FF),
|
||||||
|
),
|
||||||
|
label: const Text(
|
||||||
|
'EDIT ROUTE',
|
||||||
|
style: TextStyle(color: Color(0xFF00E5FF)),
|
||||||
|
),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
side: const BorderSide(
|
side: const BorderSide(
|
||||||
color: Color(0xFF00E5FF)),
|
color: Color(0xFF00E5FF),
|
||||||
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 10),
|
vertical: 10,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
|
key: const ValueKey('route_detail_delete_button'),
|
||||||
onPressed: _isDeleting
|
onPressed: _isDeleting
|
||||||
? null
|
? null
|
||||||
: () => _confirmDelete(route),
|
: () => _confirmDelete(route),
|
||||||
icon: const Icon(Icons.delete_outline,
|
icon: const Icon(
|
||||||
size: 16, color: Color(0xFFFF5252)),
|
Icons.delete_outline,
|
||||||
label: const Text('DELETE',
|
size: 16,
|
||||||
style: TextStyle(color: Color(0xFFFF5252))),
|
color: Color(0xFFFF5252),
|
||||||
|
),
|
||||||
|
label: const Text(
|
||||||
|
'DELETE',
|
||||||
|
style: TextStyle(color: Color(0xFFFF5252)),
|
||||||
|
),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
side: const BorderSide(
|
side: const BorderSide(
|
||||||
color: Color(0xFFFF5252)),
|
color: Color(0xFFFF5252),
|
||||||
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 10, horizontal: 16),
|
vertical: 10,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -409,14 +447,23 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
// Attached Group Ride button if any
|
// Attached Group Ride button if any
|
||||||
if (route.attachedGroupRideId != null) ...[
|
if (route.attachedGroupRideId != null) ...[
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
|
key: const ValueKey(
|
||||||
|
'route_detail_view_attached_group_ride_button',
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.push(
|
context.push(
|
||||||
'/group-rides/${route.attachedGroupRideId}');
|
'/group-rides/${route.attachedGroupRideId}',
|
||||||
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.group,
|
icon: const Icon(
|
||||||
size: 18, color: Color(0xFF00E5FF)),
|
Icons.group,
|
||||||
label: const Text('VIEW ATTACHED GROUP RIDE',
|
size: 18,
|
||||||
style: TextStyle(color: Color(0xFF00E5FF))),
|
color: Color(0xFF00E5FF),
|
||||||
|
),
|
||||||
|
label: const Text(
|
||||||
|
'VIEW ATTACHED GROUP RIDE',
|
||||||
|
style: TextStyle(color: Color(0xFF00E5FF)),
|
||||||
|
),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
side: const BorderSide(color: Color(0xFF2A2E3D)),
|
side: const BorderSide(color: Color(0xFF2A2E3D)),
|
||||||
minimumSize: const Size(double.infinity, 44),
|
minimumSize: const Size(double.infinity, 44),
|
||||||
|
|
@ -445,7 +492,8 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final stop = route.stops[index];
|
final stop = route.stops[index];
|
||||||
final isFirst = index == 0;
|
final isFirst = index == 0;
|
||||||
final isLast = index == route.stops.length - 1 &&
|
final isLast =
|
||||||
|
index == route.stops.length - 1 &&
|
||||||
route.stops.length > 1;
|
route.stops.length > 1;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
|
@ -454,7 +502,9 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF13151A),
|
color: const Color(0xFF13151A),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: const Color(0xFF2A2E3D)),
|
border: Border.all(
|
||||||
|
color: const Color(0xFF2A2E3D),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -465,8 +515,8 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
color: isFirst
|
color: isFirst
|
||||||
? const Color(0xFF00E676)
|
? const Color(0xFF00E676)
|
||||||
: isLast
|
: isLast
|
||||||
? const Color(0xFFFF5252)
|
? const Color(0xFFFF5252)
|
||||||
: const Color(0xFF00E5FF),
|
: const Color(0xFF00E5FF),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
|
@ -487,9 +537,11 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(_getIconForStopType(stop.stopType),
|
Icon(
|
||||||
size: 14,
|
_getIconForStopType(stop.stopType),
|
||||||
color: const Color(0xFF00E5FF)),
|
size: 14,
|
||||||
|
color: const Color(0xFF00E5FF),
|
||||||
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -540,7 +592,9 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
backgroundColor: const Color(0xFF00E5FF),
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 10, vertical: 6),
|
horizontal: 10,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
textStyle: const TextStyle(
|
textStyle: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -557,6 +611,9 @@ class _RouteDetailScreenState extends ConsumerState<RouteDetailScreen> {
|
||||||
// Primary action: Navigate to First Stop
|
// Primary action: Navigate to First Stop
|
||||||
if (route.stops.isNotEmpty)
|
if (route.stops.isNotEmpty)
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
|
key: const ValueKey(
|
||||||
|
'route_detail_navigate_start_button',
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final firstStop = route.stops.first;
|
final firstStop = route.stops.first;
|
||||||
NavigationHandoffService.showHandoffSheet(
|
NavigationHandoffService.showHandoffSheet(
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
}) {
|
}) {
|
||||||
final state = ref.read(routePlannerNotifierProvider);
|
final state = ref.read(routePlannerNotifierProvider);
|
||||||
final isPro = ref.read(isProSubscriberProvider);
|
final isPro = ref.read(isProSubscriberProvider);
|
||||||
final maxAllowed = isPro ? RouteService.maxTechnicalStops : RouteService.freeStopCap;
|
final maxAllowed = isPro
|
||||||
|
? RouteService.maxTechnicalStops
|
||||||
|
: RouteService.freeStopCap;
|
||||||
|
|
||||||
if (existingStop == null && state.stops.length >= maxAllowed) {
|
if (existingStop == null && state.stops.length >= maxAllowed) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|
@ -145,10 +147,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
: (initialLng?.toStringAsFixed(6) ?? ''),
|
: (initialLng?.toStringAsFixed(6) ?? ''),
|
||||||
);
|
);
|
||||||
final notesCtrl = TextEditingController(text: existingStop?.notes ?? '');
|
final notesCtrl = TextEditingController(text: existingStop?.notes ?? '');
|
||||||
RouteStopType selectedType = existingStop?.stopType ??
|
RouteStopType selectedType =
|
||||||
(state.stops.isEmpty
|
existingStop?.stopType ??
|
||||||
? RouteStopType.origin
|
(state.stops.isEmpty ? RouteStopType.origin : RouteStopType.waypoint);
|
||||||
: RouteStopType.waypoint);
|
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -206,8 +207,11 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
value: type,
|
value: type,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(_getIconForStopType(type),
|
Icon(
|
||||||
color: const Color(0xFF00E5FF), size: 18),
|
_getIconForStopType(type),
|
||||||
|
color: const Color(0xFF00E5FF),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -235,7 +239,10 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
decimal: true,
|
decimal: true,
|
||||||
signed: true,
|
signed: true,
|
||||||
),
|
),
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Latitude *',
|
labelText: 'Latitude *',
|
||||||
labelStyle: TextStyle(color: Colors.white60),
|
labelStyle: TextStyle(color: Colors.white60),
|
||||||
|
|
@ -252,7 +259,10 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
decimal: true,
|
decimal: true,
|
||||||
signed: true,
|
signed: true,
|
||||||
),
|
),
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Longitude *',
|
labelText: 'Longitude *',
|
||||||
labelStyle: TextStyle(color: Colors.white60),
|
labelStyle: TextStyle(color: Colors.white60),
|
||||||
|
|
@ -281,7 +291,10 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(dialogCtx),
|
onPressed: () => Navigator.pop(dialogCtx),
|
||||||
child: const Text('CANCEL', style: TextStyle(color: Colors.white54)),
|
child: const Text(
|
||||||
|
'CANCEL',
|
||||||
|
style: TextStyle(color: Colors.white54),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|
@ -295,24 +308,32 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
|
|
||||||
if (name.isEmpty) {
|
if (name.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Please provide a stop name.')),
|
const SnackBar(
|
||||||
|
content: Text('Please provide a stop name.'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (lat == null || lat < -90.0 || lat > 90.0) {
|
if (lat == null || lat < -90.0 || lat > 90.0) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Invalid latitude (-90 to 90).')),
|
const SnackBar(
|
||||||
|
content: Text('Invalid latitude (-90 to 90).'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (lng == null || lng < -180.0 || lng > 180.0) {
|
if (lng == null || lng < -180.0 || lng > 180.0) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Invalid longitude (-180 to 180).')),
|
const SnackBar(
|
||||||
|
content: Text('Invalid longitude (-180 to 180).'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final notifier = ref.read(routePlannerNotifierProvider.notifier);
|
final notifier = ref.read(
|
||||||
|
routePlannerNotifierProvider.notifier,
|
||||||
|
);
|
||||||
if (existingStop != null) {
|
if (existingStop != null) {
|
||||||
notifier.updateStop(
|
notifier.updateStop(
|
||||||
existingStop.copyWith(
|
existingStop.copyWith(
|
||||||
|
|
@ -362,7 +383,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
|
|
||||||
final isPro = ref.read(isProSubscriberProvider);
|
final isPro = ref.read(isProSubscriberProvider);
|
||||||
final state = ref.read(routePlannerNotifierProvider);
|
final state = ref.read(routePlannerNotifierProvider);
|
||||||
final maxAllowed = isPro ? RouteService.maxTechnicalStops : RouteService.freeStopCap;
|
final maxAllowed = isPro
|
||||||
|
? RouteService.maxTechnicalStops
|
||||||
|
: RouteService.freeStopCap;
|
||||||
|
|
||||||
if (state.stops.length > maxAllowed) {
|
if (state.stops.length > maxAllowed) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -447,13 +470,17 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
if (!_initialized) {
|
if (!_initialized) {
|
||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
backgroundColor: Color(0xFF0D0E11),
|
backgroundColor: Color(0xFF0D0E11),
|
||||||
body: Center(child: CircularProgressIndicator(color: Color(0xFF00E5FF))),
|
body: Center(
|
||||||
|
child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final state = ref.watch(routePlannerNotifierProvider);
|
final state = ref.watch(routePlannerNotifierProvider);
|
||||||
final isPro = ref.watch(isProSubscriberProvider);
|
final isPro = ref.watch(isProSubscriberProvider);
|
||||||
final maxAllowed = isPro ? RouteService.maxTechnicalStops : RouteService.freeStopCap;
|
final maxAllowed = isPro
|
||||||
|
? RouteService.maxTechnicalStops
|
||||||
|
: RouteService.freeStopCap;
|
||||||
final isAtLimit = state.stops.length >= maxAllowed;
|
final isAtLimit = state.stops.length >= maxAllowed;
|
||||||
|
|
||||||
// Center map on first stop or default
|
// Center map on first stop or default
|
||||||
|
|
@ -464,7 +491,8 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
// Build markers
|
// Build markers
|
||||||
final markers = state.stops.map((stop) {
|
final markers = state.stops.map((stop) {
|
||||||
final isFirst = stop.orderIndex == 0;
|
final isFirst = stop.orderIndex == 0;
|
||||||
final isLast = stop.orderIndex == state.stops.length - 1 && state.stops.length > 1;
|
final isLast =
|
||||||
|
stop.orderIndex == state.stops.length - 1 && state.stops.length > 1;
|
||||||
|
|
||||||
return AppMapMarker(
|
return AppMapMarker(
|
||||||
id: stop.id,
|
id: stop.id,
|
||||||
|
|
@ -476,8 +504,8 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
color: isFirst
|
color: isFirst
|
||||||
? const Color(0xFF00E676)
|
? const Color(0xFF00E676)
|
||||||
: isLast
|
: isLast
|
||||||
? const Color(0xFFFF5252)
|
? const Color(0xFFFF5252)
|
||||||
: const Color(0xFF00E5FF),
|
: const Color(0xFF00E5FF),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
boxShadow: const [
|
boxShadow: const [
|
||||||
BoxShadow(color: Colors.black54, blurRadius: 4, spreadRadius: 1),
|
BoxShadow(color: Colors.black54, blurRadius: 4, spreadRadius: 1),
|
||||||
|
|
@ -551,6 +579,7 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
|
key: const ValueKey('route_planner_save_button'),
|
||||||
onPressed: _saveRoute,
|
onPressed: _saveRoute,
|
||||||
icon: const Icon(Icons.save, color: Color(0xFF00E5FF), size: 18),
|
icon: const Icon(Icons.save, color: Color(0xFF00E5FF), size: 18),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
|
|
@ -574,11 +603,17 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
color: const Color(0xFFFF5252).withValues(alpha: 0.15),
|
color: const Color(0xFFFF5252).withValues(alpha: 0.15),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.error_outline,
|
const Icon(
|
||||||
color: Color(0xFFFF5252), size: 18),
|
Icons.error_outline,
|
||||||
|
color: Color(0xFFFF5252),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -590,7 +625,11 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.close, color: Colors.white54, size: 16),
|
icon: const Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: Colors.white54,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
onPressed: () => setState(() => _errorMessage = null),
|
onPressed: () => setState(() => _errorMessage = null),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(),
|
constraints: const BoxConstraints(),
|
||||||
|
|
@ -618,7 +657,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
right: 8,
|
right: 8,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 10, vertical: 6),
|
horizontal: 10,
|
||||||
|
vertical: 6,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF13151A).withValues(alpha: 0.9),
|
color: const Color(0xFF13151A).withValues(alpha: 0.9),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
|
@ -643,11 +684,17 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 6, vertical: 2),
|
horizontal: 6,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isAtLimit
|
color: isAtLimit
|
||||||
? const Color(0xFFFFB300).withValues(alpha: 0.2)
|
? const Color(
|
||||||
: const Color(0xFF00E5FF).withValues(alpha: 0.15),
|
0xFFFFB300,
|
||||||
|
).withValues(alpha: 0.2)
|
||||||
|
: const Color(
|
||||||
|
0xFF00E5FF,
|
||||||
|
).withValues(alpha: 0.15),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -700,7 +747,10 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
),
|
),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: 'Route Title (e.g. Skyline Ridge Loop) *',
|
hintText: 'Route Title (e.g. Skyline Ridge Loop) *',
|
||||||
hintStyle: TextStyle(color: Colors.white30, fontSize: 14),
|
hintStyle: TextStyle(
|
||||||
|
color: Colors.white30,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 8),
|
contentPadding: EdgeInsets.symmetric(vertical: 8),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
|
|
@ -721,8 +771,11 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.visibility_outlined,
|
const Icon(
|
||||||
size: 14, color: Colors.white54),
|
Icons.visibility_outlined,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.white54,
|
||||||
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
_visibility.label,
|
_visibility.label,
|
||||||
|
|
@ -732,30 +785,36 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Icon(Icons.arrow_drop_down,
|
const Icon(
|
||||||
size: 16, color: Color(0xFF00E5FF)),
|
Icons.arrow_drop_down,
|
||||||
|
size: 16,
|
||||||
|
color: Color(0xFF00E5FF),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
itemBuilder: (context) => RouteVisibility.values.map((v) {
|
itemBuilder: (context) =>
|
||||||
return PopupMenuItem(
|
RouteVisibility.values.map((v) {
|
||||||
value: v,
|
return PopupMenuItem(
|
||||||
child: Text(
|
value: v,
|
||||||
v.label,
|
child: Text(
|
||||||
style: const TextStyle(
|
v.label,
|
||||||
color: Colors.white,
|
style: const TextStyle(
|
||||||
fontSize: 13,
|
color: Colors.white,
|
||||||
),
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}).toList(),
|
);
|
||||||
|
}).toList(),
|
||||||
),
|
),
|
||||||
if (!isPro && isAtLimit)
|
if (!isPro && isAtLimit)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context.push('/settings/pro'),
|
onPressed: () => context.push('/settings/pro'),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8, vertical: 4),
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
minimumSize: Size.zero,
|
minimumSize: Size.zero,
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
),
|
),
|
||||||
|
|
@ -778,7 +837,10 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
|
|
||||||
// Stop List Controls (Add Stop / Add GPS / Reverse)
|
// Stop List Controls (Add Stop / Add GPS / Reverse)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
|
|
@ -789,6 +851,7 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
|
key: const ValueKey('route_planner_add_stop_button'),
|
||||||
onPressed: () => _showAddOrEditStopDialog(),
|
onPressed: () => _showAddOrEditStopDialog(),
|
||||||
icon: const Icon(Icons.add_location_alt, size: 16),
|
icon: const Icon(Icons.add_location_alt, size: 16),
|
||||||
label: Text(isNarrow ? 'ADD' : 'ADD STOP'),
|
label: Text(isNarrow ? 'ADD' : 'ADD STOP'),
|
||||||
|
|
@ -796,7 +859,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
backgroundColor: const Color(0xFF00E5FF),
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 10, vertical: 8),
|
horizontal: 10,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
textStyle: const TextStyle(
|
textStyle: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -805,15 +870,25 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
|
key: const ValueKey(
|
||||||
|
'route_planner_current_gps_button',
|
||||||
|
),
|
||||||
onPressed: _addCurrentLocationStop,
|
onPressed: _addCurrentLocationStop,
|
||||||
icon: const Icon(Icons.my_location,
|
icon: const Icon(
|
||||||
size: 16, color: Color(0xFF00E5FF)),
|
Icons.my_location,
|
||||||
label: Text(isNarrow ? 'GPS' : 'CURRENT GPS',
|
size: 16,
|
||||||
style: const TextStyle(color: Color(0xFF00E5FF))),
|
color: Color(0xFF00E5FF),
|
||||||
|
),
|
||||||
|
label: Text(
|
||||||
|
isNarrow ? 'GPS' : 'CURRENT GPS',
|
||||||
|
style: const TextStyle(color: Color(0xFF00E5FF)),
|
||||||
|
),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
side: const BorderSide(color: Color(0xFF2A2E3D)),
|
side: const BorderSide(color: Color(0xFF2A2E3D)),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 10, vertical: 8),
|
horizontal: 10,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
textStyle: const TextStyle(fontSize: 12),
|
textStyle: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -821,15 +896,24 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
),
|
),
|
||||||
if (state.stops.length >= 2)
|
if (state.stops.length >= 2)
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () =>
|
key: const ValueKey('route_planner_reverse_button'),
|
||||||
ref.read(routePlannerNotifierProvider.notifier).reverseStops(),
|
onPressed: () => ref
|
||||||
icon: const Icon(Icons.swap_vert,
|
.read(routePlannerNotifierProvider.notifier)
|
||||||
size: 16, color: Colors.white70),
|
.reverseStops(),
|
||||||
label: const Text('REVERSE',
|
icon: const Icon(
|
||||||
style: TextStyle(color: Colors.white70, fontSize: 11)),
|
Icons.swap_vert,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
label: const Text(
|
||||||
|
'REVERSE',
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 11),
|
||||||
|
),
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 6, vertical: 4),
|
horizontal: 6,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
minimumSize: Size.zero,
|
minimumSize: Size.zero,
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
),
|
),
|
||||||
|
|
@ -845,27 +929,35 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.alt_route,
|
Icon(
|
||||||
size: 48,
|
Icons.alt_route,
|
||||||
color: Colors.white.withValues(alpha: 0.15)),
|
size: 48,
|
||||||
|
color: Colors.white.withValues(alpha: 0.15),
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
const Text(
|
const Text(
|
||||||
'No stops added yet.',
|
'No stops added yet.',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white54, fontSize: 14),
|
color: Colors.white54,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
const Text(
|
const Text(
|
||||||
'Tap "Add Stop" or "Current GPS" to begin planning.',
|
'Tap "Add Stop" or "Current GPS" to begin planning.',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white30, fontSize: 12),
|
color: Colors.white30,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: ReorderableListView.builder(
|
: ReorderableListView.builder(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16, vertical: 4),
|
horizontal: 16,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
itemCount: state.stops.length,
|
itemCount: state.stops.length,
|
||||||
// ignore: deprecated_member_use
|
// ignore: deprecated_member_use
|
||||||
onReorder: (oldIdx, newIdx) {
|
onReorder: (oldIdx, newIdx) {
|
||||||
|
|
@ -877,7 +969,8 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
final stop = state.stops[index];
|
final stop = state.stops[index];
|
||||||
final isFirst = index == 0;
|
final isFirst = index == 0;
|
||||||
final isLast =
|
final isLast =
|
||||||
index == state.stops.length - 1 && state.stops.length > 1;
|
index == state.stops.length - 1 &&
|
||||||
|
state.stops.length > 1;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
key: ValueKey(stop.id),
|
key: ValueKey(stop.id),
|
||||||
|
|
@ -886,7 +979,9 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF13151A),
|
color: const Color(0xFF13151A),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: const Color(0xFF2A2E3D)),
|
border: Border.all(
|
||||||
|
color: const Color(0xFF2A2E3D),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -898,8 +993,8 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
color: isFirst
|
color: isFirst
|
||||||
? const Color(0xFF00E676)
|
? const Color(0xFF00E676)
|
||||||
: isLast
|
: isLast
|
||||||
? const Color(0xFFFF5252)
|
? const Color(0xFFFF5252)
|
||||||
: const Color(0xFF00E5FF),
|
: const Color(0xFF00E5FF),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
|
|
@ -917,7 +1012,8 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
// Stop Details
|
// Stop Details
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -966,8 +1062,11 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
|
|
||||||
// Actions: Handoff navigation, Edit, Delete
|
// Actions: Handoff navigation, Edit, Delete
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.navigation,
|
icon: const Icon(
|
||||||
color: Color(0xFF00E5FF), size: 18),
|
Icons.navigation,
|
||||||
|
color: Color(0xFF00E5FF),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
tooltip: 'Open in Maps',
|
tooltip: 'Open in Maps',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
NavigationHandoffService.showHandoffSheet(
|
NavigationHandoffService.showHandoffSheet(
|
||||||
|
|
@ -978,33 +1077,50 @@ class _RoutePlannerScreenState extends ConsumerState<RoutePlannerScreen> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(minWidth: 32),
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 32,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit,
|
icon: const Icon(
|
||||||
color: Colors.white60, size: 18),
|
Icons.edit,
|
||||||
|
color: Colors.white60,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
tooltip: 'Edit stop',
|
tooltip: 'Edit stop',
|
||||||
onPressed: () => _showAddOrEditStopDialog(
|
onPressed: () => _showAddOrEditStopDialog(
|
||||||
existingStop: stop,
|
existingStop: stop,
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(minWidth: 32),
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 32,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.delete_outline,
|
icon: const Icon(
|
||||||
color: Color(0xFFFF5252), size: 18),
|
Icons.delete_outline,
|
||||||
|
color: Color(0xFFFF5252),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
tooltip: 'Remove stop',
|
tooltip: 'Remove stop',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref
|
ref
|
||||||
.read(routePlannerNotifierProvider.notifier)
|
.read(
|
||||||
|
routePlannerNotifierProvider.notifier,
|
||||||
|
)
|
||||||
.removeStop(stop.id);
|
.removeStop(stop.id);
|
||||||
},
|
},
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
constraints: const BoxConstraints(minWidth: 32),
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 32,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
const Icon(Icons.drag_handle,
|
const Icon(
|
||||||
color: Colors.white30, size: 20),
|
Icons.drag_handle,
|
||||||
|
color: Colors.white30,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||||
final user = authState.user;
|
final user = authState.user;
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
|
key: const ValueKey('settings_account_card'),
|
||||||
onTap: () => context.go('/settings/account'),
|
onTap: () => context.go('/settings/account'),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@ class CurrentPlanCard extends ConsumerWidget {
|
||||||
if (!isPro) ...[
|
if (!isPro) ...[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
|
key: const ValueKey('settings_upgrade_to_pro_button'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
backgroundColor: const Color(0xFF00E5FF),
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
|
|
@ -218,6 +219,7 @@ class CurrentPlanCard extends ConsumerWidget {
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
],
|
],
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
|
key: const ValueKey('settings_pro_details_button'),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: Colors.white70,
|
foregroundColor: Colors.white70,
|
||||||
side: const BorderSide(color: Colors.white24),
|
side: const BorderSide(color: Colors.white24),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:marionette_flutter/marionette_flutter.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
|
|
@ -11,7 +14,13 @@ import 'features/records/records_service.dart';
|
||||||
import 'services/sync/sync_observer.dart';
|
import 'services/sync/sync_observer.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
final isFlutterTest =
|
||||||
|
!kIsWeb && Platform.environment.containsKey('FLUTTER_TEST');
|
||||||
|
if (kDebugMode && !isFlutterTest) {
|
||||||
|
MarionetteBinding.ensureInitialized();
|
||||||
|
} else {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Firebase.initializeApp(
|
await Firebase.initializeApp(
|
||||||
|
|
@ -68,7 +77,9 @@ class _MotorcycleDashAppState extends ConsumerState<MotorcycleDashApp> {
|
||||||
|
|
||||||
void _handleDeepLink(Uri uri) {
|
void _handleDeepLink(Uri uri) {
|
||||||
final uriStr = uri.toString();
|
final uriStr = uri.toString();
|
||||||
if (uriStr.contains('callback') || uri.host == 'oauth' || uri.scheme == 'motorsignal') {
|
if (uriStr.contains('callback') ||
|
||||||
|
uri.host == 'oauth' ||
|
||||||
|
uri.scheme == 'motorsignal') {
|
||||||
try {
|
try {
|
||||||
appRouter.go('/oauth/callback?${uri.query}');
|
appRouter.go('/oauth/callback?${uri.query}');
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,11 @@ class NavigationHandoffService {
|
||||||
required double longitude,
|
required double longitude,
|
||||||
String? displayName,
|
String? displayName,
|
||||||
}) {
|
}) {
|
||||||
final dest = '${latitude.toStringAsFixed(6)},${longitude.toStringAsFixed(6)}';
|
final dest =
|
||||||
return Uri.parse('https://www.google.com/maps/dir/?api=1&destination=$dest');
|
'${latitude.toStringAsFixed(6)},${longitude.toStringAsFixed(6)}';
|
||||||
|
return Uri.parse(
|
||||||
|
'https://www.google.com/maps/dir/?api=1&destination=$dest',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a standard Waze navigation URI.
|
/// Builds a standard Waze navigation URI.
|
||||||
|
|
@ -51,7 +54,8 @@ class NavigationHandoffService {
|
||||||
required double longitude,
|
required double longitude,
|
||||||
required String displayName,
|
required String displayName,
|
||||||
}) {
|
}) {
|
||||||
final coords = '${latitude.toStringAsFixed(6)},${longitude.toStringAsFixed(6)}';
|
final coords =
|
||||||
|
'${latitude.toStringAsFixed(6)},${longitude.toStringAsFixed(6)}';
|
||||||
return Uri.parse('geo:$coords?q=$coords($displayName)');
|
return Uri.parse('geo:$coords?q=$coords($displayName)');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +83,11 @@ class NavigationHandoffService {
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.navigation, color: Color(0xFF00E5FF), size: 22),
|
const Icon(
|
||||||
|
Icons.navigation,
|
||||||
|
color: Color(0xFF00E5FF),
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -112,6 +120,7 @@ class NavigationHandoffService {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
key: const ValueKey('handoff_google_maps_tile'),
|
||||||
leading: Container(
|
leading: Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -120,9 +129,22 @@ class NavigationHandoffService {
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.map, color: Colors.white),
|
child: const Icon(Icons.map, color: Colors.white),
|
||||||
),
|
),
|
||||||
title: const Text('Google Maps', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
|
title: const Text(
|
||||||
subtitle: const Text('Open turn-by-turn in Google Maps', style: TextStyle(color: Colors.white54, fontSize: 12)),
|
'Google Maps',
|
||||||
trailing: const Icon(Icons.open_in_new, color: Colors.white38, size: 18),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Open turn-by-turn in Google Maps',
|
||||||
|
style: TextStyle(color: Colors.white54, fontSize: 12),
|
||||||
|
),
|
||||||
|
trailing: const Icon(
|
||||||
|
Icons.open_in_new,
|
||||||
|
color: Colors.white38,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Navigator.pop(sheetContext);
|
Navigator.pop(sheetContext);
|
||||||
final uri = buildGoogleMapsUri(
|
final uri = buildGoogleMapsUri(
|
||||||
|
|
@ -131,23 +153,43 @@ class NavigationHandoffService {
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
);
|
);
|
||||||
if (await canLaunchUrl(uri)) {
|
if (await canLaunchUrl(uri)) {
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
await launchUrl(
|
||||||
|
uri,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(color: Colors.white12, height: 1),
|
const Divider(color: Colors.white12, height: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
key: const ValueKey('handoff_waze_tile'),
|
||||||
leading: Container(
|
leading: Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white10,
|
color: Colors.white10,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.directions_car, color: Color(0xFF00E5FF)),
|
child: const Icon(
|
||||||
|
Icons.directions_car,
|
||||||
|
color: Color(0xFF00E5FF),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: const Text(
|
||||||
|
'Waze',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Open road directions in Waze',
|
||||||
|
style: TextStyle(color: Colors.white54, fontSize: 12),
|
||||||
|
),
|
||||||
|
trailing: const Icon(
|
||||||
|
Icons.open_in_new,
|
||||||
|
color: Colors.white38,
|
||||||
|
size: 18,
|
||||||
),
|
),
|
||||||
title: const Text('Waze', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
|
|
||||||
subtitle: const Text('Open road directions in Waze', style: TextStyle(color: Colors.white54, fontSize: 12)),
|
|
||||||
trailing: const Icon(Icons.open_in_new, color: Colors.white38, size: 18),
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Navigator.pop(sheetContext);
|
Navigator.pop(sheetContext);
|
||||||
final uri = buildWazeUri(
|
final uri = buildWazeUri(
|
||||||
|
|
@ -155,12 +197,16 @@ class NavigationHandoffService {
|
||||||
longitude: longitude,
|
longitude: longitude,
|
||||||
);
|
);
|
||||||
if (await canLaunchUrl(uri)) {
|
if (await canLaunchUrl(uri)) {
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
await launchUrl(
|
||||||
|
uri,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(color: Colors.white12, height: 1),
|
const Divider(color: Colors.white12, height: 1),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
key: const ValueKey('handoff_system_map_tile'),
|
||||||
leading: Container(
|
leading: Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -169,9 +215,22 @@ class NavigationHandoffService {
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.place, color: Colors.white70),
|
child: const Icon(Icons.place, color: Colors.white70),
|
||||||
),
|
),
|
||||||
title: const Text('System Map', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
|
title: const Text(
|
||||||
subtitle: const Text('Open in default platform mapping app', style: TextStyle(color: Colors.white54, fontSize: 12)),
|
'System Map',
|
||||||
trailing: const Icon(Icons.open_in_new, color: Colors.white38, size: 18),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Open in default platform mapping app',
|
||||||
|
style: TextStyle(color: Colors.white54, fontSize: 12),
|
||||||
|
),
|
||||||
|
trailing: const Icon(
|
||||||
|
Icons.open_in_new,
|
||||||
|
color: Colors.white38,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
Navigator.pop(sheetContext);
|
Navigator.pop(sheetContext);
|
||||||
final uri = buildSystemGeoUri(
|
final uri = buildSystemGeoUri(
|
||||||
|
|
@ -180,7 +239,10 @@ class NavigationHandoffService {
|
||||||
displayName: displayName,
|
displayName: displayName,
|
||||||
);
|
);
|
||||||
if (await canLaunchUrl(uri)) {
|
if (await canLaunchUrl(uri)) {
|
||||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
await launchUrl(
|
||||||
|
uri,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -209,10 +271,7 @@ class NavigationHandoffService {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NavigationApp.waze:
|
case NavigationApp.waze:
|
||||||
uri = buildWazeUri(
|
uri = buildWazeUri(latitude: stop.latitude, longitude: stop.longitude);
|
||||||
latitude: stop.latitude,
|
|
||||||
longitude: stop.longitude,
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NavigationApp.platformDefault:
|
case NavigationApp.platformDefault:
|
||||||
|
|
@ -232,7 +291,10 @@ class NavigationHandoffService {
|
||||||
'https://www.google.com/maps/search/?api=1&query=${stop.latitude},${stop.longitude}',
|
'https://www.google.com/maps/search/?api=1&query=${stop.latitude},${stop.longitude}',
|
||||||
);
|
);
|
||||||
if (await canLaunchUrl(fallbackUri)) {
|
if (await canLaunchUrl(fallbackUri)) {
|
||||||
return await launchUrl(fallbackUri, mode: LaunchMode.externalApplication);
|
return await launchUrl(
|
||||||
|
fallbackUri,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -262,8 +324,9 @@ class NavigationHandoffService {
|
||||||
.map((s) => '${s.latitude},${s.longitude}')
|
.map((s) => '${s.latitude},${s.longitude}')
|
||||||
.join('|');
|
.join('|');
|
||||||
|
|
||||||
final waypointsParam =
|
final waypointsParam = waypointsStr.isNotEmpty
|
||||||
waypointsStr.isNotEmpty ? '&waypoints=$waypointsStr' : '';
|
? '&waypoints=$waypointsStr'
|
||||||
|
: '';
|
||||||
final uri = Uri.parse(
|
final uri = Uri.parse(
|
||||||
'https://www.google.com/maps/dir/?api=1&origin=$origin&destination=$dest$waypointsParam',
|
'https://www.google.com/maps/dir/?api=1&origin=$origin&destination=$dest$waypointsParam',
|
||||||
);
|
);
|
||||||
|
|
@ -273,7 +336,9 @@ class NavigationHandoffService {
|
||||||
return await launchUrl(uri, mode: LaunchMode.externalApplication);
|
return await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('[NavigationHandoffService] Failed to launch multi-stop route: $e');
|
debugPrint(
|
||||||
|
'[NavigationHandoffService] Failed to launch multi-stop route: $e',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -925,6 +925,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
marionette_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: marionette_flutter
|
||||||
|
sha256: "9bf6aa531157edeb3d94f7c4972367240a1d704bbe9f4ac558ef1a125c4d1a6d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ dependencies:
|
||||||
cloud_firestore: ^5.6.5
|
cloud_firestore: ^5.6.5
|
||||||
flutter_secure_storage: ^10.2.1
|
flutter_secure_storage: ^10.2.1
|
||||||
in_app_purchase: 3.3.0
|
in_app_purchase: 3.3.0
|
||||||
|
marionette_flutter: ^0.6.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue