feat: unify group rides and social experience
This commit is contained in:
parent
ce6641102d
commit
4beacfde03
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import '../../core/responsive/responsive_layout.dart';
|
||||||
|
import '../../core/responsive/viewport_size.dart';
|
||||||
|
import '../../core/theme/theme_extensions.dart';
|
||||||
import '../../services/auth/auth_providers.dart';
|
import '../../services/auth/auth_providers.dart';
|
||||||
import '../../services/group_rides/group_ride_models.dart';
|
import '../../services/group_rides/group_ride_models.dart';
|
||||||
import '../../services/group_rides/group_ride_providers.dart';
|
import '../../services/group_rides/group_ride_providers.dart';
|
||||||
|
|
@ -31,19 +34,20 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = context.motorColors;
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
final isAuthed = user != null && !user.isAnonymous;
|
final isAuthed = user != null && !user.isAnonymous;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF0D0E11),
|
backgroundColor: colors.surfaceContainer,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: const Color(0xFF13151A),
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: const Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.two_wheeler, color: Color(0xFF00E5FF), size: 24),
|
Icon(Icons.two_wheeler, color: colors.cyberCyan, size: 24),
|
||||||
SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Text(
|
const Text(
|
||||||
'GROUP RIDES',
|
'GROUP RIDES',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
|
|
@ -55,9 +59,9 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
),
|
),
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
indicatorColor: const Color(0xFF00E5FF),
|
indicatorColor: colors.cyberCyan,
|
||||||
indicatorWeight: 3,
|
indicatorWeight: 3,
|
||||||
labelColor: const Color(0xFF00E5FF),
|
labelColor: colors.cyberCyan,
|
||||||
unselectedLabelColor: Colors.white60,
|
unselectedLabelColor: Colors.white60,
|
||||||
labelStyle: const TextStyle(
|
labelStyle: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -73,25 +77,30 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
if (isAuthed)
|
if (isAuthed)
|
||||||
IconButton(
|
IconButton(
|
||||||
key: const ValueKey('group_rides_create_appbar_button'),
|
key: const ValueKey('group_rides_create_appbar_button'),
|
||||||
icon: const Icon(Icons.add, color: Color(0xFF00E5FF)),
|
icon: Icon(Icons.add, color: colors.cyberCyan),
|
||||||
tooltip: 'Create Group Ride',
|
tooltip: 'Create Group Ride',
|
||||||
onPressed: () => context.push('/group-rides/create'),
|
onPressed: () => context.push('/group-rides/create'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: !isAuthed
|
body: ResponsiveLayout(
|
||||||
? _buildAccountRequiredNotice(context)
|
builder: (context, snapshot) {
|
||||||
: TabBarView(
|
if (!isAuthed) {
|
||||||
controller: _tabController,
|
return _buildAccountRequiredNotice(context, colors);
|
||||||
children: [
|
}
|
||||||
_buildUpcomingTab(context, ref, user.uid),
|
return TabBarView(
|
||||||
_buildMyRidesTab(context, ref, user.uid),
|
controller: _tabController,
|
||||||
],
|
children: [
|
||||||
),
|
_buildUpcomingTab(context, ref, user.uid, snapshot, colors),
|
||||||
|
_buildMyRidesTab(context, ref, user.uid, snapshot, colors),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
floatingActionButton: isAuthed
|
floatingActionButton: isAuthed
|
||||||
? FloatingActionButton.extended(
|
? FloatingActionButton.extended(
|
||||||
key: const ValueKey('create_group_ride_fab'),
|
key: const ValueKey('create_group_ride_fab'),
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
backgroundColor: colors.cyberCyan,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
icon: const Icon(Icons.add_road),
|
icon: const Icon(Icons.add_road),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
|
|
@ -104,9 +113,12 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAccountRequiredNotice(BuildContext context) {
|
Widget _buildAccountRequiredNotice(
|
||||||
|
BuildContext context,
|
||||||
|
MotorSignalThemeColors colors,
|
||||||
|
) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
|
@ -115,12 +127,12 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: const Color(0xFF00E5FF).withValues(alpha: 0.1),
|
color: colors.cyberCyan.withValues(alpha: 0.1),
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
Icons.groups_outlined,
|
Icons.groups_outlined,
|
||||||
size: 64,
|
size: 64,
|
||||||
color: Color(0xFF00E5FF),
|
color: colors.cyberCyan,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
@ -145,7 +157,7 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
backgroundColor: colors.cyberCyan,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 24,
|
horizontal: 24,
|
||||||
|
|
@ -172,6 +184,8 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
String currentUid,
|
String currentUid,
|
||||||
|
ResponsiveSnapshot snapshot,
|
||||||
|
MotorSignalThemeColors colors,
|
||||||
) {
|
) {
|
||||||
final ridesAsync = ref.watch(upcomingGroupRidesProvider);
|
final ridesAsync = ref.watch(upcomingGroupRidesProvider);
|
||||||
|
|
||||||
|
|
@ -179,6 +193,7 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
data: (rides) {
|
data: (rides) {
|
||||||
if (rides.isEmpty) {
|
if (rides.isEmpty) {
|
||||||
return _buildEmptyState(
|
return _buildEmptyState(
|
||||||
|
colors: colors,
|
||||||
icon: Icons.map_outlined,
|
icon: Icons.map_outlined,
|
||||||
title: 'No Upcoming Group Rides',
|
title: 'No Upcoming Group Rides',
|
||||||
message:
|
message:
|
||||||
|
|
@ -188,22 +203,43 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isWide = snapshot.sizeClass == WindowSizeClass.expanded ||
|
||||||
|
(snapshot.isLandscape && snapshot.width >= 720);
|
||||||
|
|
||||||
|
if (isWide) {
|
||||||
|
final crossAxisCount = snapshot.width >= 1100 ? 3 : 2;
|
||||||
|
return GridView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
crossAxisSpacing: 16,
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
mainAxisExtent: 185,
|
||||||
|
),
|
||||||
|
itemCount: rides.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final ride = rides[index];
|
||||||
|
return _buildRideCard(context, ride, currentUid, colors);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: rides.length,
|
itemCount: rides.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final ride = rides[index];
|
final ride = rides[index];
|
||||||
return _buildRideCard(context, ride, currentUid);
|
return _buildRideCard(context, ride, currentUid, colors);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(
|
loading: () => Center(
|
||||||
child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
|
child: CircularProgressIndicator(color: colors.cyberCyan),
|
||||||
),
|
),
|
||||||
error: (e, _) => Center(
|
error: (e, _) => Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Error loading rides: $e',
|
'Error loading rides: $e',
|
||||||
style: const TextStyle(color: Colors.redAccent),
|
style: TextStyle(color: colors.raceRed),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -213,6 +249,8 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
String currentUid,
|
String currentUid,
|
||||||
|
ResponsiveSnapshot snapshot,
|
||||||
|
MotorSignalThemeColors colors,
|
||||||
) {
|
) {
|
||||||
final ridesAsync = ref.watch(upcomingGroupRidesProvider);
|
final ridesAsync = ref.watch(upcomingGroupRidesProvider);
|
||||||
|
|
||||||
|
|
@ -224,6 +262,7 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
|
|
||||||
if (myRides.isEmpty) {
|
if (myRides.isEmpty) {
|
||||||
return _buildEmptyState(
|
return _buildEmptyState(
|
||||||
|
colors: colors,
|
||||||
icon: Icons.sports_motorsports_outlined,
|
icon: Icons.sports_motorsports_outlined,
|
||||||
title: 'You Haven\'t Created Any Rides',
|
title: 'You Haven\'t Created Any Rides',
|
||||||
message:
|
message:
|
||||||
|
|
@ -233,28 +272,50 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isWide = snapshot.sizeClass == WindowSizeClass.expanded ||
|
||||||
|
(snapshot.isLandscape && snapshot.width >= 720);
|
||||||
|
|
||||||
|
if (isWide) {
|
||||||
|
final crossAxisCount = snapshot.width >= 1100 ? 3 : 2;
|
||||||
|
return GridView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
crossAxisSpacing: 16,
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
mainAxisExtent: 185,
|
||||||
|
),
|
||||||
|
itemCount: myRides.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final ride = myRides[index];
|
||||||
|
return _buildRideCard(context, ride, currentUid, colors);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: myRides.length,
|
itemCount: myRides.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final ride = myRides[index];
|
final ride = myRides[index];
|
||||||
return _buildRideCard(context, ride, currentUid);
|
return _buildRideCard(context, ride, currentUid, colors);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(
|
loading: () => Center(
|
||||||
child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
|
child: CircularProgressIndicator(color: colors.cyberCyan),
|
||||||
),
|
),
|
||||||
error: (e, _) => Center(
|
error: (e, _) => Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Error: $e',
|
'Error: $e',
|
||||||
style: const TextStyle(color: Colors.redAccent),
|
style: TextStyle(color: colors.raceRed),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState({
|
Widget _buildEmptyState({
|
||||||
|
required MotorSignalThemeColors colors,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String title,
|
required String title,
|
||||||
required String message,
|
required String message,
|
||||||
|
|
@ -286,8 +347,8 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: const Color(0xFF00E5FF),
|
foregroundColor: colors.cyberCyan,
|
||||||
side: const BorderSide(color: Color(0xFF00E5FF)),
|
side: BorderSide(color: colors.cyberCyan),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
|
|
@ -305,18 +366,19 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
GroupRide ride,
|
GroupRide ride,
|
||||||
String currentUid,
|
String currentUid,
|
||||||
|
MotorSignalThemeColors colors,
|
||||||
) {
|
) {
|
||||||
final isOrganizer = ride.organizerUid == currentUid;
|
final isOrganizer = ride.organizerUid == currentUid;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
color: const Color(0xFF161922),
|
color: colors.surfaceContainerHigh,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: isOrganizer
|
color: isOrganizer
|
||||||
? const Color(0xFF00E5FF).withValues(alpha: 0.4)
|
? colors.cyberCyan.withValues(alpha: 0.4)
|
||||||
: Colors.white12,
|
: colors.outlineVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
|
|
@ -337,8 +399,8 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ride.isCanceled
|
color: ride.isCanceled
|
||||||
? Colors.redAccent.withValues(alpha: 0.2)
|
? colors.raceRed.withValues(alpha: 0.2)
|
||||||
: const Color(0xFF00E5FF).withValues(alpha: 0.15),
|
: colors.cyberCyan.withValues(alpha: 0.15),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -347,8 +409,8 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: ride.isCanceled
|
color: ride.isCanceled
|
||||||
? Colors.redAccent
|
? colors.raceRed
|
||||||
: const Color(0xFF00E5FF),
|
: colors.cyberCyan,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -360,15 +422,15 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
vertical: 2,
|
vertical: 2,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.amber.withValues(alpha: 0.2),
|
color: colors.warningAmber.withValues(alpha: 0.2),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'ORGANIZER',
|
'ORGANIZER',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.amber,
|
color: colors.warningAmber,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -396,6 +458,8 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Text(
|
Text(
|
||||||
ride.title,
|
ride.title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -405,18 +469,21 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.schedule,
|
Icons.schedule,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: Color(0xFF00E5FF),
|
color: colors.cyberCyan,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Expanded(
|
||||||
'Meet: ${_formatDateTime(ride.scheduledStartAt)} • KSU: ${_formatTime(ride.departureAt)}',
|
child: Text(
|
||||||
style: const TextStyle(
|
'Meet: ${_formatDateTime(ride.scheduledStartAt)} • KSU: ${_formatTime(ride.departureAt)}',
|
||||||
fontSize: 12,
|
overflow: TextOverflow.ellipsis,
|
||||||
color: Colors.white70,
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
fontSize: 12,
|
||||||
|
color: Colors.white70,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -446,10 +513,10 @@ class _GroupRidesHubScreenState extends ConsumerState<GroupRidesHubScreen>
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.flag_outlined,
|
Icons.flag_outlined,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: Color(0xFFFFB300),
|
color: colors.warningAmber,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import '../../core/responsive/responsive_layout.dart';
|
||||||
|
import '../../core/theme/theme_extensions.dart';
|
||||||
import '../../services/auth/auth_providers.dart';
|
import '../../services/auth/auth_providers.dart';
|
||||||
import '../../services/group_rides/group_ride_models.dart';
|
import '../../services/group_rides/group_ride_models.dart';
|
||||||
import '../../services/group_rides/group_ride_providers.dart';
|
import '../../services/group_rides/group_ride_providers.dart';
|
||||||
|
|
@ -48,6 +50,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = context.motorColors;
|
||||||
final rideAsync = ref.watch(groupRideStreamProvider(widget.rideId));
|
final rideAsync = ref.watch(groupRideStreamProvider(widget.rideId));
|
||||||
final participantsAsync = ref.watch(
|
final participantsAsync = ref.watch(
|
||||||
groupRideParticipantsProvider(widget.rideId),
|
groupRideParticipantsProvider(widget.rideId),
|
||||||
|
|
@ -68,9 +71,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
data: (ride) {
|
data: (ride) {
|
||||||
if (ride == null) {
|
if (ride == null) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF0D0E11),
|
backgroundColor: colors.surfaceContainer,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: const Color(0xFF13151A),
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
title: const Text('LIVE GROUP RIDE'),
|
title: const Text('LIVE GROUP RIDE'),
|
||||||
),
|
),
|
||||||
body: const Center(
|
body: const Center(
|
||||||
|
|
@ -88,9 +91,9 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
// Check if ride was closed
|
// Check if ride was closed
|
||||||
if (ride.isCompleted || ride.isCanceled) {
|
if (ride.isCompleted || ride.isCanceled) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF0D0E11),
|
backgroundColor: colors.surfaceContainer,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: const Color(0xFF13151A),
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
title: Text(
|
title: Text(
|
||||||
ride.title.toUpperCase(),
|
ride.title.toUpperCase(),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
|
|
@ -109,8 +112,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
Icon(
|
Icon(
|
||||||
ride.isCompleted ? Icons.flag : Icons.cancel,
|
ride.isCompleted ? Icons.flag : Icons.cancel,
|
||||||
color: ride.isCompleted
|
color: ride.isCompleted
|
||||||
? Colors.greenAccent
|
? colors.launchGreen
|
||||||
: Colors.redAccent,
|
: colors.raceRed,
|
||||||
size: 48,
|
size: 48,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -128,7 +131,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
backgroundColor: colors.cyberCyan,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
),
|
),
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
|
|
@ -202,7 +205,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
points: attachedRoute.previewPoints
|
points: attachedRoute.previewPoints
|
||||||
.map((p) => AppLatLng(p.latitude, p.longitude))
|
.map((p) => AppLatLng(p.latitude, p.longitude))
|
||||||
.toList(),
|
.toList(),
|
||||||
color: const Color(0xFF00E5FF).withValues(alpha: 0.85),
|
color: colors.cyberCyan.withValues(alpha: 0.85),
|
||||||
width: 3.5,
|
width: 3.5,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -217,10 +220,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
currentUid: currentUid,
|
currentUid: currentUid,
|
||||||
leadUid: leadParticipant?.userId,
|
leadUid: leadParticipant?.userId,
|
||||||
attachedRoute: attachedRoute,
|
attachedRoute: attachedRoute,
|
||||||
|
colors: colors,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFF0D0E11),
|
backgroundColor: colors.surfaceContainer,
|
||||||
appBar: _buildAppBar(
|
appBar: _buildAppBar(
|
||||||
context: context,
|
context: context,
|
||||||
ride: ride,
|
ride: ride,
|
||||||
|
|
@ -229,137 +233,153 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
participants: acceptedParticipants,
|
participants: acceptedParticipants,
|
||||||
presences: presences,
|
presences: presences,
|
||||||
leadPresence: leadPresence,
|
leadPresence: leadPresence,
|
||||||
|
colors: colors,
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: ResponsiveLayout(
|
||||||
children: [
|
builder: (context, snapshot) {
|
||||||
// 1. Unified Map
|
final isShort = snapshot.height < 450;
|
||||||
Positioned.fill(
|
final isLandscapeWide =
|
||||||
child: UnifiedMapView(
|
snapshot.isLandscape && snapshot.width >= 720;
|
||||||
center: center,
|
|
||||||
zoom: _mapZoom,
|
|
||||||
markers: markers,
|
|
||||||
polylines: polylines,
|
|
||||||
onPositionChanged: (pos) {
|
|
||||||
_customMapCenter = pos;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 2. Top Banners (Regroup, Master Privacy)
|
return Stack(
|
||||||
Positioned(
|
children: [
|
||||||
top: 8,
|
// 1. Unified Map
|
||||||
left: 8,
|
Positioned.fill(
|
||||||
right: 8,
|
child: UnifiedMapView(
|
||||||
child: Column(
|
center: center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
zoom: _mapZoom,
|
||||||
children: [
|
markers: markers,
|
||||||
if (regroupCaller != null)
|
polylines: polylines,
|
||||||
_buildRegroupBanner(
|
onPositionChanged: (pos) {
|
||||||
context: context,
|
_customMapCenter = pos;
|
||||||
caller: regroupCaller,
|
},
|
||||||
participants: acceptedParticipants,
|
),
|
||||||
isLeadOrOrganizer:
|
|
||||||
isOrganizer ||
|
|
||||||
(leadParticipant != null &&
|
|
||||||
leadParticipant.userId == currentUid),
|
|
||||||
controller: broadcastController,
|
|
||||||
),
|
|
||||||
if (!broadcastState.isMasterAllowed)
|
|
||||||
_buildMasterPrivacyBanner(context, ref),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 3. Quick Status Bar & Broadcast Floating Control
|
|
||||||
Positioned(
|
|
||||||
bottom: 12,
|
|
||||||
left: 8,
|
|
||||||
right: 8,
|
|
||||||
child: SafeArea(
|
|
||||||
top: false,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// Quick Status Selector
|
|
||||||
_buildQuickStatusBar(
|
|
||||||
currentStatus: broadcastState.status,
|
|
||||||
isBroadcasting: broadcastState.isBroadcasting,
|
|
||||||
onStatusSelected: (s) =>
|
|
||||||
broadcastController.setStatus(s),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
|
|
||||||
// Bottom Control Strip (Opt-in toggle, Roster opener, Recenter)
|
|
||||||
_buildBottomControlStrip(
|
|
||||||
context: context,
|
|
||||||
ride: ride,
|
|
||||||
broadcastState: broadcastState,
|
|
||||||
broadcastController: broadcastController,
|
|
||||||
participants: acceptedParticipants,
|
|
||||||
presences: presences,
|
|
||||||
leadPresence: leadPresence,
|
|
||||||
currentUid: currentUid,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
// 2. Top Banners (Regroup, Master Privacy)
|
||||||
],
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
left: 8,
|
||||||
|
right: isLandscapeWide ? snapshot.width * 0.35 : 8,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (regroupCaller != null)
|
||||||
|
_buildRegroupBanner(
|
||||||
|
context: context,
|
||||||
|
caller: regroupCaller,
|
||||||
|
participants: acceptedParticipants,
|
||||||
|
isLeadOrOrganizer:
|
||||||
|
isOrganizer ||
|
||||||
|
(leadParticipant != null &&
|
||||||
|
leadParticipant.userId ==
|
||||||
|
currentUid),
|
||||||
|
controller: broadcastController,
|
||||||
|
colors: colors,
|
||||||
|
),
|
||||||
|
if (!broadcastState.isMasterAllowed)
|
||||||
|
_buildMasterPrivacyBanner(
|
||||||
|
context, ref, colors),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 3. Quick Status Bar & Broadcast Floating Control
|
||||||
|
Positioned(
|
||||||
|
bottom: isShort ? 6 : 12,
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
child: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Quick Status Selector
|
||||||
|
_buildQuickStatusBar(
|
||||||
|
currentStatus: broadcastState.status,
|
||||||
|
isBroadcasting:
|
||||||
|
broadcastState.isBroadcasting,
|
||||||
|
onStatusSelected: (s) =>
|
||||||
|
broadcastController.setStatus(s),
|
||||||
|
colors: colors,
|
||||||
|
isShort: isShort,
|
||||||
|
),
|
||||||
|
SizedBox(height: isShort ? 6 : 8),
|
||||||
|
|
||||||
|
// Bottom Control Strip (Opt-in toggle, Roster opener, Recenter)
|
||||||
|
_buildBottomControlStrip(
|
||||||
|
context: context,
|
||||||
|
ride: ride,
|
||||||
|
broadcastState: broadcastState,
|
||||||
|
broadcastController: broadcastController,
|
||||||
|
participants: acceptedParticipants,
|
||||||
|
presences: presences,
|
||||||
|
leadPresence: leadPresence,
|
||||||
|
currentUid: currentUid,
|
||||||
|
colors: colors,
|
||||||
|
isShort: isShort,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Scaffold(
|
loading: () => Scaffold(
|
||||||
backgroundColor: Color(0xFF0D0E11),
|
backgroundColor: colors.surfaceContainer,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
|
child: CircularProgressIndicator(color: colors.cyberCyan),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
error: (e, _) => Scaffold(
|
error: (e, _) => Scaffold(
|
||||||
backgroundColor: const Color(0xFF0D0E11),
|
backgroundColor: colors.surfaceContainer,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Error loading live presence: $e',
|
'Error loading live presence: $e',
|
||||||
style: const TextStyle(color: Colors.redAccent),
|
style: TextStyle(color: colors.raceRed),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Scaffold(
|
loading: () => Scaffold(
|
||||||
backgroundColor: Color(0xFF0D0E11),
|
backgroundColor: colors.surfaceContainer,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
|
child: CircularProgressIndicator(color: colors.cyberCyan),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
error: (e, _) => Scaffold(
|
error: (e, _) => Scaffold(
|
||||||
backgroundColor: const Color(0xFF0D0E11),
|
backgroundColor: colors.surfaceContainer,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Error loading participants: $e',
|
'Error loading participants: $e',
|
||||||
style: const TextStyle(color: Colors.redAccent),
|
style: TextStyle(color: colors.raceRed),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Scaffold(
|
loading: () => Scaffold(
|
||||||
backgroundColor: Color(0xFF0D0E11),
|
backgroundColor: colors.surfaceContainer,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: CircularProgressIndicator(color: Color(0xFF00E5FF)),
|
child: CircularProgressIndicator(color: colors.cyberCyan),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
error: (e, _) => Scaffold(
|
error: (e, _) => Scaffold(
|
||||||
backgroundColor: const Color(0xFF0D0E11),
|
backgroundColor: colors.surfaceContainer,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Error loading ride: $e',
|
'Error loading ride: $e',
|
||||||
style: const TextStyle(color: Colors.redAccent),
|
style: TextStyle(color: colors.raceRed),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PreferredSizeWidget _buildAppBar({
|
PreferredSizeWidget _buildAppBar({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required GroupRide ride,
|
required GroupRide ride,
|
||||||
|
|
@ -368,9 +388,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
required List<GroupRideParticipant> participants,
|
required List<GroupRideParticipant> participants,
|
||||||
required List<LiveGroupPresence> presences,
|
required List<LiveGroupPresence> presences,
|
||||||
required LiveGroupPresence? leadPresence,
|
required LiveGroupPresence? leadPresence,
|
||||||
|
required MotorSignalThemeColors colors,
|
||||||
}) {
|
}) {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
backgroundColor: const Color(0xFF13151A),
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
elevation: 3,
|
elevation: 3,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
|
@ -399,8 +420,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ride.status == GroupRideStatus.underway
|
color: ride.status == GroupRideStatus.underway
|
||||||
? const Color(0xFF00E5FF).withValues(alpha: 0.2)
|
? colors.cyberCyan.withValues(alpha: 0.2)
|
||||||
: Colors.amber.withValues(alpha: 0.2),
|
: colors.warningAmber.withValues(alpha: 0.2),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -409,8 +430,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: ride.status == GroupRideStatus.underway
|
color: ride.status == GroupRideStatus.underway
|
||||||
? const Color(0xFF00E5FF)
|
? colors.cyberCyan
|
||||||
: Colors.amber,
|
: colors.warningAmber,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -425,15 +446,15 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
actions: [
|
actions: [
|
||||||
if (isOrganizer)
|
if (isOrganizer)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: Icon(
|
||||||
Icons.stop_circle_outlined,
|
Icons.stop_circle_outlined,
|
||||||
color: Colors.redAccent,
|
color: colors.raceRed,
|
||||||
),
|
),
|
||||||
tooltip: 'End Ride',
|
tooltip: 'End Ride',
|
||||||
onPressed: () => _confirmEndRide(context, ride, currentUid),
|
onPressed: () => _confirmEndRide(context, ride, currentUid, colors),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.people_outline, color: Color(0xFF00E5FF)),
|
icon: Icon(Icons.people_outline, color: colors.cyberCyan),
|
||||||
tooltip: 'Roster',
|
tooltip: 'Roster',
|
||||||
onPressed: () => _showRosterSheet(
|
onPressed: () => _showRosterSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
@ -441,6 +462,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
presences: presences,
|
presences: presences,
|
||||||
leadPresence: leadPresence,
|
leadPresence: leadPresence,
|
||||||
currentUid: currentUid,
|
currentUid: currentUid,
|
||||||
|
colors: colors,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -453,6 +475,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
required List<GroupRideParticipant> participants,
|
required List<GroupRideParticipant> participants,
|
||||||
required bool isLeadOrOrganizer,
|
required bool isLeadOrOrganizer,
|
||||||
required LiveBroadcastController controller,
|
required LiveBroadcastController controller,
|
||||||
|
required MotorSignalThemeColors colors,
|
||||||
}) {
|
}) {
|
||||||
String callerName = caller.userId;
|
String callerName = caller.userId;
|
||||||
try {
|
try {
|
||||||
|
|
@ -466,7 +489,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
margin: const EdgeInsets.only(bottom: 6),
|
margin: const EdgeInsets.only(bottom: 6),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.amber.withValues(alpha: 0.92),
|
color: colors.warningAmber.withValues(alpha: 0.92),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
boxShadow: const [
|
boxShadow: const [
|
||||||
BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)),
|
BoxShadow(color: Colors.black45, blurRadius: 4, offset: Offset(0, 2)),
|
||||||
|
|
@ -536,18 +559,19 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMasterPrivacyBanner(BuildContext context, WidgetRef ref) {
|
Widget _buildMasterPrivacyBanner(
|
||||||
|
BuildContext context, WidgetRef ref, MotorSignalThemeColors colors) {
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 6),
|
margin: const EdgeInsets.only(bottom: 6),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1E222D),
|
color: colors.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: Colors.amber.withValues(alpha: 0.6)),
|
border: Border.all(color: colors.warningAmber.withValues(alpha: 0.6)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.lock_outline, color: Colors.amber, size: 18),
|
Icon(Icons.lock_outline, color: colors.warningAmber, size: 18),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Expanded(
|
const Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -558,7 +582,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
TextButton(
|
TextButton(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
backgroundColor: Colors.amber,
|
backgroundColor: colors.warningAmber,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
minimumSize: Size.zero,
|
minimumSize: Size.zero,
|
||||||
|
|
@ -581,6 +605,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
required LivePresenceStatus currentStatus,
|
required LivePresenceStatus currentStatus,
|
||||||
required bool isBroadcasting,
|
required bool isBroadcasting,
|
||||||
required ValueChanged<LivePresenceStatus> onStatusSelected,
|
required ValueChanged<LivePresenceStatus> onStatusSelected,
|
||||||
|
required MotorSignalThemeColors colors,
|
||||||
|
bool isShort = false,
|
||||||
}) {
|
}) {
|
||||||
final statuses = [
|
final statuses = [
|
||||||
(LivePresenceStatus.riding, 'RIDING', Icons.two_wheeler),
|
(LivePresenceStatus.riding, 'RIDING', Icons.two_wheeler),
|
||||||
|
|
@ -592,11 +618,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
];
|
];
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 46,
|
height: isShort ? 40 : 46,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF161922).withValues(alpha: 0.95),
|
color: colors.surfaceContainerHigh.withValues(alpha: 0.95),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: Colors.white12),
|
border: Border.all(color: colors.outlineVariant),
|
||||||
boxShadow: const [
|
boxShadow: const [
|
||||||
BoxShadow(color: Colors.black54, blurRadius: 6, offset: Offset(0, 2)),
|
BoxShadow(color: Colors.black54, blurRadius: 6, offset: Offset(0, 2)),
|
||||||
],
|
],
|
||||||
|
|
@ -610,13 +636,13 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
final (status, label, icon) = statuses[index];
|
final (status, label, icon) = statuses[index];
|
||||||
final isSelected = currentStatus == status;
|
final isSelected = currentStatus == status;
|
||||||
|
|
||||||
Color activeColor = const Color(0xFF00E5FF);
|
Color activeColor = colors.cyberCyan;
|
||||||
if (status == LivePresenceStatus.needFuel)
|
if (status == LivePresenceStatus.needFuel)
|
||||||
activeColor = Colors.orangeAccent;
|
activeColor = Colors.orangeAccent;
|
||||||
if (status == LivePresenceStatus.regrouping)
|
if (status == LivePresenceStatus.regrouping)
|
||||||
activeColor = Colors.amber;
|
activeColor = colors.warningAmber;
|
||||||
if (status == LivePresenceStatus.arrived)
|
if (status == LivePresenceStatus.arrived)
|
||||||
activeColor = Colors.greenAccent;
|
activeColor = colors.launchGreen;
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
|
@ -670,13 +696,15 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
required List<LiveGroupPresence> presences,
|
required List<LiveGroupPresence> presences,
|
||||||
required LiveGroupPresence? leadPresence,
|
required LiveGroupPresence? leadPresence,
|
||||||
required String? currentUid,
|
required String? currentUid,
|
||||||
|
required MotorSignalThemeColors colors,
|
||||||
|
bool isShort = false,
|
||||||
}) {
|
}) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: isShort ? 4 : 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF161922).withValues(alpha: 0.95),
|
color: colors.surfaceContainerHigh.withValues(alpha: 0.95),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(color: Colors.white12),
|
border: Border.all(color: colors.outlineVariant),
|
||||||
boxShadow: const [
|
boxShadow: const [
|
||||||
BoxShadow(color: Colors.black54, blurRadius: 6, offset: Offset(0, 2)),
|
BoxShadow(color: Colors.black54, blurRadius: 6, offset: Offset(0, 2)),
|
||||||
],
|
],
|
||||||
|
|
@ -692,7 +720,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
? Icons.radar
|
? Icons.radar
|
||||||
: Icons.location_off,
|
: Icons.location_off,
|
||||||
color: broadcastState.isBroadcasting
|
color: broadcastState.isBroadcasting
|
||||||
? const Color(0xFF00E5FF)
|
? colors.cyberCyan
|
||||||
: Colors.white38,
|
: Colors.white38,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
|
|
@ -714,8 +742,8 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: broadcastState.isBroadcasting
|
color: broadcastState.isBroadcasting
|
||||||
? (broadcastState.isPaused
|
? (broadcastState.isPaused
|
||||||
? Colors.amber
|
? colors.warningAmber
|
||||||
: const Color(0xFF00E5FF))
|
: colors.cyberCyan)
|
||||||
: Colors.white60,
|
: Colors.white60,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
|
|
@ -743,7 +771,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
key: const ValueKey('live_ride_broadcast_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: colors.cyberCyan,
|
||||||
onChanged: (val) => broadcastController.setOptIn(val),
|
onChanged: (val) => broadcastController.setOptIn(val),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -752,18 +780,18 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
// Recenter Map Button
|
// Recenter Map Button (min 48x48 tap target)
|
||||||
IconButton(
|
IconButton(
|
||||||
key: const ValueKey('live_ride_recenter_button'),
|
key: const ValueKey('live_ride_recenter_button'),
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.white10,
|
backgroundColor: colors.surfaceContainer,
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
minimumSize: const Size(36, 36),
|
minimumSize: const Size(48, 48),
|
||||||
),
|
),
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.my_location,
|
Icons.my_location,
|
||||||
color: Colors.white70,
|
color: Colors.white70,
|
||||||
size: 18,
|
size: 20,
|
||||||
),
|
),
|
||||||
tooltip: 'Center on Me',
|
tooltip: 'Center on Me',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
|
@ -792,13 +820,13 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
key: const ValueKey('live_ride_roster_button'),
|
key: const ValueKey('live_ride_roster_button'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF00E5FF).withValues(alpha: 0.15),
|
backgroundColor: colors.cyberCyan.withValues(alpha: 0.15),
|
||||||
foregroundColor: const Color(0xFF00E5FF),
|
foregroundColor: colors.cyberCyan,
|
||||||
side: const BorderSide(color: Color(0xFF00E5FF)),
|
side: BorderSide(color: colors.cyberCyan),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||||
minimumSize: Size.zero,
|
minimumSize: const Size(48, 48),
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.format_list_bulleted, size: 15),
|
icon: const Icon(Icons.format_list_bulleted, size: 16),
|
||||||
label: Text(
|
label: Text(
|
||||||
'${participants.length}',
|
'${participants.length}',
|
||||||
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
|
||||||
|
|
@ -809,6 +837,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
presences: presences,
|
presences: presences,
|
||||||
leadPresence: leadPresence,
|
leadPresence: leadPresence,
|
||||||
currentUid: currentUid,
|
currentUid: currentUid,
|
||||||
|
colors: colors,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -822,10 +851,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
required List<LiveGroupPresence> presences,
|
required List<LiveGroupPresence> presences,
|
||||||
required LiveGroupPresence? leadPresence,
|
required LiveGroupPresence? leadPresence,
|
||||||
required String? currentUid,
|
required String? currentUid,
|
||||||
|
required MotorSignalThemeColors colors,
|
||||||
}) {
|
}) {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: const Color(0xFF161922),
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
),
|
),
|
||||||
|
|
@ -835,12 +865,12 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.people, color: Color(0xFF00E5FF), size: 18),
|
Icon(Icons.people, color: colors.cyberCyan, size: 18),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text(
|
Text(
|
||||||
'LIVE RIDER ROSTER',
|
'LIVE RIDER ROSTER',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF00E5FF),
|
color: colors.cyberCyan,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
letterSpacing: 1.0,
|
letterSpacing: 1.0,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|
@ -853,13 +883,14 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(height: 20, color: Colors.white12),
|
Divider(height: 20, color: colors.outlineVariant),
|
||||||
for (final p in participants) ...[
|
for (final p in participants) ...[
|
||||||
_buildRosterItem(
|
_buildRosterItem(
|
||||||
participant: p,
|
participant: p,
|
||||||
presence: _findPresence(presences, p.userId),
|
presence: _findPresence(presences, p.userId),
|
||||||
leadPresence: leadPresence,
|
leadPresence: leadPresence,
|
||||||
isCaller: p.userId == currentUid,
|
isCaller: p.userId == currentUid,
|
||||||
|
colors: colors,
|
||||||
onCenter: (pos) {
|
onCenter: (pos) {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -881,6 +912,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
required LiveGroupPresence? presence,
|
required LiveGroupPresence? presence,
|
||||||
required LiveGroupPresence? leadPresence,
|
required LiveGroupPresence? leadPresence,
|
||||||
required bool isCaller,
|
required bool isCaller,
|
||||||
|
required MotorSignalThemeColors colors,
|
||||||
required ValueChanged<AppLatLng> onCenter,
|
required ValueChanged<AppLatLng> onCenter,
|
||||||
}) {
|
}) {
|
||||||
// Freshness
|
// Freshness
|
||||||
|
|
@ -910,12 +942,12 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF1B1E28),
|
color: colors.surfaceContainer,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isCaller
|
color: isCaller
|
||||||
? const Color(0xFF00E5FF).withValues(alpha: 0.5)
|
? colors.cyberCyan.withValues(alpha: 0.5)
|
||||||
: Colors.white10,
|
: colors.outlineVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -925,7 +957,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
children: [
|
children: [
|
||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 18,
|
radius: 18,
|
||||||
backgroundColor: Colors.white10,
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
child: Text(
|
child: Text(
|
||||||
participant.riderDisplayName.isNotEmpty
|
participant.riderDisplayName.isNotEmpty
|
||||||
? participant.riderDisplayName[0].toUpperCase()
|
? participant.riderDisplayName[0].toUpperCase()
|
||||||
|
|
@ -937,12 +969,12 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isLive)
|
if (isLive)
|
||||||
const Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 5,
|
radius: 5,
|
||||||
backgroundColor: Colors.greenAccent,
|
backgroundColor: colors.launchGreen,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -965,7 +997,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isCaller
|
color: isCaller
|
||||||
? const Color(0xFF00E5FF)
|
? colors.cyberCyan
|
||||||
: Colors.white,
|
: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
|
|
@ -982,6 +1014,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _roleColor(
|
color: _roleColor(
|
||||||
participant.role,
|
participant.role,
|
||||||
|
colors,
|
||||||
).withValues(alpha: 0.2),
|
).withValues(alpha: 0.2),
|
||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(3),
|
||||||
),
|
),
|
||||||
|
|
@ -990,7 +1023,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: _roleColor(participant.role),
|
color: _roleColor(participant.role, colors),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -1007,7 +1040,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: _statusColor(presence.status),
|
color: _statusColor(presence.status, colors),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
|
@ -1041,18 +1074,18 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
isLive ? 'LIVE' : '${presence.secondsAgo}s ago',
|
isLive ? 'LIVE' : '${presence.secondsAgo}s ago',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isLive
|
color: isLive
|
||||||
? Colors.greenAccent
|
? colors.launchGreen
|
||||||
: (isRecent ? Colors.amber : Colors.white38),
|
: (isRecent ? colors.warningAmber : Colors.white38),
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: Icon(
|
||||||
Icons.gps_fixed,
|
Icons.gps_fixed,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: Color(0xFF00E5FF),
|
color: colors.cyberCyan,
|
||||||
),
|
),
|
||||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
|
|
@ -1070,12 +1103,12 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Color _roleColor(GroupRideRole role) {
|
Color _roleColor(GroupRideRole role, MotorSignalThemeColors colors) {
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case GroupRideRole.organizer:
|
case GroupRideRole.organizer:
|
||||||
return const Color(0xFF00E5FF);
|
return colors.cyberCyan;
|
||||||
case GroupRideRole.lead:
|
case GroupRideRole.lead:
|
||||||
return const Color(0xFFFFD700);
|
return colors.warningAmber;
|
||||||
case GroupRideRole.sweep:
|
case GroupRideRole.sweep:
|
||||||
return Colors.orangeAccent;
|
return Colors.orangeAccent;
|
||||||
case GroupRideRole.participant:
|
case GroupRideRole.participant:
|
||||||
|
|
@ -1083,20 +1116,20 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Color _statusColor(LivePresenceStatus status) {
|
Color _statusColor(LivePresenceStatus status, MotorSignalThemeColors colors) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case LivePresenceStatus.riding:
|
case LivePresenceStatus.riding:
|
||||||
return Colors.greenAccent;
|
return colors.launchGreen;
|
||||||
case LivePresenceStatus.stopped:
|
case LivePresenceStatus.stopped:
|
||||||
return Colors.white70;
|
return Colors.white70;
|
||||||
case LivePresenceStatus.needFuel:
|
case LivePresenceStatus.needFuel:
|
||||||
return Colors.orangeAccent;
|
return Colors.orangeAccent;
|
||||||
case LivePresenceStatus.regrouping:
|
case LivePresenceStatus.regrouping:
|
||||||
return Colors.amber;
|
return colors.warningAmber;
|
||||||
case LivePresenceStatus.delayed:
|
case LivePresenceStatus.delayed:
|
||||||
return Colors.redAccent;
|
return colors.raceRed;
|
||||||
case LivePresenceStatus.arrived:
|
case LivePresenceStatus.arrived:
|
||||||
return const Color(0xFF00E5FF);
|
return colors.cyberCyan;
|
||||||
case LivePresenceStatus.offline:
|
case LivePresenceStatus.offline:
|
||||||
return Colors.white38;
|
return Colors.white38;
|
||||||
}
|
}
|
||||||
|
|
@ -1136,6 +1169,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
required String? currentUid,
|
required String? currentUid,
|
||||||
required String? leadUid,
|
required String? leadUid,
|
||||||
MotorSignalRoute? attachedRoute,
|
MotorSignalRoute? attachedRoute,
|
||||||
|
required MotorSignalThemeColors colors,
|
||||||
}) {
|
}) {
|
||||||
final markers = <AppMapMarker>[];
|
final markers = <AppMapMarker>[];
|
||||||
|
|
||||||
|
|
@ -1152,10 +1186,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
: 'Meetup',
|
: 'Meetup',
|
||||||
iconWidget: Container(
|
iconWidget: Container(
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.greenAccent,
|
color: colors.launchGreen,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
boxShadow: [
|
boxShadow: const [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black45,
|
color: Colors.black45,
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
|
|
@ -1186,10 +1220,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
label: stop.displayName,
|
label: stop.displayName,
|
||||||
iconWidget: Container(
|
iconWidget: Container(
|
||||||
padding: const EdgeInsets.all(5),
|
padding: const EdgeInsets.all(5),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Color(0xFF00E5FF),
|
color: colors.cyberCyan,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
boxShadow: [
|
boxShadow: const [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black45,
|
color: Colors.black45,
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
|
|
@ -1276,10 +1310,10 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
: 'Destination',
|
: 'Destination',
|
||||||
iconWidget: Container(
|
iconWidget: Container(
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.redAccent,
|
color: colors.raceRed,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
boxShadow: [
|
boxShadow: const [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black45,
|
color: Colors.black45,
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
|
|
@ -1310,11 +1344,11 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
final isCaller = presence.userId == currentUid;
|
final isCaller = presence.userId == currentUid;
|
||||||
|
|
||||||
// Color scheme
|
// Color scheme
|
||||||
Color markerColor = const Color(0xFF00E5FF);
|
Color markerColor = colors.cyberCyan;
|
||||||
if (isLead) markerColor = const Color(0xFFFFD700); // Gold for lead
|
if (isLead) markerColor = colors.warningAmber; // Gold for lead
|
||||||
if (isSweep) markerColor = Colors.orangeAccent; // Orange for sweep
|
if (isSweep) markerColor = Colors.orangeAccent; // Orange for sweep
|
||||||
if (presence.status == LivePresenceStatus.regrouping)
|
if (presence.status == LivePresenceStatus.regrouping)
|
||||||
markerColor = Colors.amber;
|
markerColor = colors.warningAmber;
|
||||||
if (presence.status == LivePresenceStatus.needFuel)
|
if (presence.status == LivePresenceStatus.needFuel)
|
||||||
markerColor = Colors.deepOrangeAccent;
|
markerColor = Colors.deepOrangeAccent;
|
||||||
|
|
||||||
|
|
@ -1367,12 +1401,13 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
GroupRide ride,
|
GroupRide ride,
|
||||||
String? currentUid,
|
String? currentUid,
|
||||||
|
MotorSignalThemeColors colors,
|
||||||
) {
|
) {
|
||||||
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: colors.surfaceContainerHigh,
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'End Group Ride?',
|
'End Group Ride?',
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||||
|
|
@ -1391,7 +1426,7 @@ class _LiveGroupRideScreenState extends ConsumerState<LiveGroupRideScreen>
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.redAccent,
|
backgroundColor: colors.raceRed,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../core/theme/evolution_theme_config.dart';
|
import '../../core/responsive/responsive_layout.dart';
|
||||||
|
import '../../core/responsive/viewport_size.dart';
|
||||||
|
import '../../core/theme/theme_extensions.dart';
|
||||||
import '../../services/auth/auth_providers.dart';
|
import '../../services/auth/auth_providers.dart';
|
||||||
import '../../services/social/rider_profile_models.dart';
|
import '../../services/social/rider_profile_models.dart';
|
||||||
import '../../services/social/social_providers.dart';
|
import '../../services/social/social_providers.dart';
|
||||||
|
|
@ -273,23 +275,18 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = context.motorColors;
|
||||||
final authState = ref.watch(authNotifierProvider);
|
final authState = ref.watch(authNotifierProvider);
|
||||||
final isAuthed = authState.isAuthenticated;
|
final isAuthed = authState.isAuthenticated;
|
||||||
final themeMode = ref.watch(dashboardThemeModeProvider);
|
|
||||||
final isEvolution = themeMode == DashboardThemeMode.evolution;
|
|
||||||
|
|
||||||
final bgColor = isEvolution
|
|
||||||
? Colors.black
|
|
||||||
: Theme.of(context).scaffoldBackgroundColor;
|
|
||||||
final titleFont = isEvolution ? 'Sora' : null;
|
|
||||||
|
|
||||||
if (!isAuthed) {
|
if (!isAuthed) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: bgColor,
|
backgroundColor: colors.surfaceContainer,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
|
title: const Text(
|
||||||
'Friends & Riders',
|
'Friends & Riders',
|
||||||
style: TextStyle(fontFamily: titleFont, fontWeight: FontWeight.w800),
|
style: TextStyle(fontWeight: FontWeight.w800),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
|
|
@ -298,16 +295,15 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.group_outlined,
|
Icons.group_outlined,
|
||||||
size: 64,
|
size: 64,
|
||||||
color: Color(0xFF00E5FF),
|
color: colors.cyberCyan,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
const Text(
|
||||||
'MOTORCLOUD ACCOUNT REQUIRED',
|
'MOTORCLOUD ACCOUNT REQUIRED',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: titleFont,
|
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|
@ -323,7 +319,7 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
backgroundColor: colors.cyberCyan,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.login),
|
icon: const Icon(Icons.login),
|
||||||
|
|
@ -345,11 +341,12 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
final friendsCount = friendsAsync.value?.length ?? 0;
|
final friendsCount = friendsAsync.value?.length ?? 0;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: bgColor,
|
backgroundColor: colors.surfaceContainer,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
|
title: const Text(
|
||||||
'Friends & Riders',
|
'Friends & Riders',
|
||||||
style: TextStyle(fontFamily: titleFont, fontWeight: FontWeight.w800),
|
style: TextStyle(fontWeight: FontWeight.w800),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
@ -360,8 +357,10 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
],
|
],
|
||||||
bottom: TabBar(
|
bottom: TabBar(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
indicatorColor: const Color(0xFF00E5FF),
|
isScrollable: true,
|
||||||
labelColor: const Color(0xFF00E5FF),
|
tabAlignment: TabAlignment.start,
|
||||||
|
indicatorColor: colors.cyberCyan,
|
||||||
|
labelColor: colors.cyberCyan,
|
||||||
unselectedLabelColor: Colors.white60,
|
unselectedLabelColor: Colors.white60,
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(text: 'FRIENDS ($friendsCount)'),
|
Tab(text: 'FRIENDS ($friendsCount)'),
|
||||||
|
|
@ -378,7 +377,7 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
vertical: 2,
|
vertical: 2,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.amberAccent,
|
color: colors.warningAmber,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|
@ -403,8 +402,15 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
children: [
|
children: [
|
||||||
// TAB 1: FRIENDS LIST
|
// TAB 1: FRIENDS LIST
|
||||||
friendsAsync.when(
|
friendsAsync.when(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => Center(
|
||||||
error: (err, _) => Center(child: Text('Error: $err')),
|
child: CircularProgressIndicator(color: colors.cyberCyan),
|
||||||
|
),
|
||||||
|
error: (err, _) => Center(
|
||||||
|
child: Text(
|
||||||
|
'Error: $err',
|
||||||
|
style: TextStyle(color: colors.raceRed),
|
||||||
|
),
|
||||||
|
),
|
||||||
data: (friends) {
|
data: (friends) {
|
||||||
if (friends.isEmpty) {
|
if (friends.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
|
|
@ -437,7 +443,7 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
backgroundColor: colors.cyberCyan,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.search),
|
icon: const Icon(Icons.search),
|
||||||
|
|
@ -450,67 +456,112 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.separated(
|
return ResponsiveLayout(
|
||||||
padding: const EdgeInsets.all(16.0),
|
builder: (context, snapshot) {
|
||||||
itemCount: friends.length,
|
final isWide = snapshot.sizeClass == WindowSizeClass.expanded ||
|
||||||
separatorBuilder: (context, index) =>
|
(snapshot.isLandscape && snapshot.width >= 720);
|
||||||
const Divider(color: Colors.white10),
|
|
||||||
itemBuilder: (context, index) {
|
if (isWide) {
|
||||||
final friend = friends[index];
|
final crossAxisCount = snapshot.width >= 1100 ? 3 : 2;
|
||||||
return ListTile(
|
return GridView.builder(
|
||||||
contentPadding: EdgeInsets.zero,
|
padding: const EdgeInsets.all(20),
|
||||||
leading: AvatarBadge(
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
avatarPreset: friend.avatarPreset,
|
crossAxisCount: crossAxisCount,
|
||||||
photoUrl: friend.avatarUrl,
|
crossAxisSpacing: 16,
|
||||||
displayName: friend.displayName,
|
mainAxisSpacing: 16,
|
||||||
radius: 24,
|
mainAxisExtent: 88,
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
friend.displayName,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
),
|
||||||
),
|
itemCount: friends.length,
|
||||||
subtitle: Text(
|
itemBuilder: (context, index) {
|
||||||
friend.displayHandle,
|
final friend = friends[index];
|
||||||
style: const TextStyle(
|
return Card(
|
||||||
color: Color(0xFF00E5FF),
|
color: colors.surfaceContainerHigh,
|
||||||
fontSize: 12,
|
elevation: 0,
|
||||||
),
|
shape: RoundedRectangleBorder(
|
||||||
),
|
borderRadius: BorderRadius.circular(12),
|
||||||
trailing: PopupMenuButton<String>(
|
side: BorderSide(color: colors.outlineVariant),
|
||||||
icon: const Icon(Icons.more_vert, color: Colors.white60),
|
),
|
||||||
onSelected: (val) {
|
child: Padding(
|
||||||
if (val == 'remove') {
|
padding: const EdgeInsets.symmetric(
|
||||||
_removeFriend(friend);
|
horizontal: 14,
|
||||||
} else if (val == 'block') {
|
vertical: 8,
|
||||||
_blockRider(friend.uid, friend.handle, friend.displayName);
|
),
|
||||||
}
|
child: Row(
|
||||||
|
children: [
|
||||||
|
AvatarBadge(
|
||||||
|
avatarPreset: friend.avatarPreset,
|
||||||
|
photoUrl: friend.avatarUrl,
|
||||||
|
displayName: friend.displayName,
|
||||||
|
radius: 24,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
friend.displayName,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
friend.displayHandle,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: colors.cyberCyan,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildFriendMenu(friend, colors),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => [
|
);
|
||||||
const PopupMenuItem(
|
}
|
||||||
value: 'remove',
|
|
||||||
child: Row(
|
return ListView.separated(
|
||||||
children: [
|
padding: const EdgeInsets.all(16.0),
|
||||||
Icon(Icons.person_remove_outlined, size: 18),
|
itemCount: friends.length,
|
||||||
SizedBox(width: 8),
|
separatorBuilder: (context, index) =>
|
||||||
Text('Remove Friend'),
|
Divider(color: colors.outlineVariant),
|
||||||
],
|
itemBuilder: (context, index) {
|
||||||
|
final friend = friends[index];
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: AvatarBadge(
|
||||||
|
avatarPreset: friend.avatarPreset,
|
||||||
|
photoUrl: friend.avatarUrl,
|
||||||
|
displayName: friend.displayName,
|
||||||
|
radius: 24,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
friend.displayName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const PopupMenuItem(
|
subtitle: Text(
|
||||||
value: 'block',
|
friend.displayHandle,
|
||||||
child: Row(
|
style: TextStyle(
|
||||||
children: [
|
color: colors.cyberCyan,
|
||||||
Icon(Icons.block, size: 18, color: Colors.redAccent),
|
fontSize: 12,
|
||||||
SizedBox(width: 8),
|
|
||||||
Text('Block Rider', style: TextStyle(color: Colors.redAccent)),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
trailing: _buildFriendMenu(friend, colors),
|
||||||
),
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -522,10 +573,10 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
children: [
|
children: [
|
||||||
// Incoming Section
|
// Incoming Section
|
||||||
const Text(
|
Text(
|
||||||
'INCOMING REQUESTS',
|
'INCOMING REQUESTS',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF00E5FF),
|
color: colors.cyberCyan,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
letterSpacing: 0.8,
|
letterSpacing: 0.8,
|
||||||
|
|
@ -533,13 +584,16 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
incomingAsync.when(
|
incomingAsync.when(
|
||||||
loading: () => const Center(
|
loading: () => Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(color: colors.cyberCyan),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
error: (err, _) => Text('Error: $err'),
|
error: (err, _) => Text(
|
||||||
|
'Error: $err',
|
||||||
|
style: TextStyle(color: colors.raceRed),
|
||||||
|
),
|
||||||
data: (requests) {
|
data: (requests) {
|
||||||
if (requests.isEmpty) {
|
if (requests.isEmpty) {
|
||||||
return const Padding(
|
return const Padding(
|
||||||
|
|
@ -554,8 +608,12 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
return Column(
|
return Column(
|
||||||
children: requests.map((req) {
|
children: requests.map((req) {
|
||||||
return Card(
|
return Card(
|
||||||
color: Colors.white.withValues(alpha: 0.05),
|
color: colors.surfaceContainerHigh,
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
side: BorderSide(color: colors.outlineVariant),
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -580,8 +638,8 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'@${req.senderHandle}',
|
'@${req.senderHandle}',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF00E5FF),
|
color: colors.cyberCyan,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -589,17 +647,17 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: Icon(
|
||||||
Icons.check_circle_outline,
|
Icons.check_circle_outline,
|
||||||
color: Colors.greenAccent,
|
color: colors.launchGreen,
|
||||||
),
|
),
|
||||||
tooltip: 'Accept',
|
tooltip: 'Accept',
|
||||||
onPressed: () => _acceptFriendRequest(req),
|
onPressed: () => _acceptFriendRequest(req),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: Icon(
|
||||||
Icons.cancel_outlined,
|
Icons.cancel_outlined,
|
||||||
color: Colors.redAccent,
|
color: colors.raceRed,
|
||||||
),
|
),
|
||||||
tooltip: 'Decline',
|
tooltip: 'Decline',
|
||||||
onPressed: () => _declineFriendRequest(req),
|
onPressed: () => _declineFriendRequest(req),
|
||||||
|
|
@ -627,7 +685,10 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
outgoingAsync.when(
|
outgoingAsync.when(
|
||||||
loading: () => const SizedBox.shrink(),
|
loading: () => const SizedBox.shrink(),
|
||||||
error: (err, _) => Text('Error: $err'),
|
error: (err, _) => Text(
|
||||||
|
'Error: $err',
|
||||||
|
style: TextStyle(color: colors.raceRed),
|
||||||
|
),
|
||||||
data: (requests) {
|
data: (requests) {
|
||||||
if (requests.isEmpty) {
|
if (requests.isEmpty) {
|
||||||
return const Padding(
|
return const Padding(
|
||||||
|
|
@ -685,10 +746,13 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _searchController,
|
controller: _searchController,
|
||||||
decoration: const InputDecoration(
|
style: const TextStyle(color: Colors.white),
|
||||||
|
decoration: InputDecoration(
|
||||||
hintText: 'Enter @handle (e.g. speed_demon)',
|
hintText: 'Enter @handle (e.g. speed_demon)',
|
||||||
prefixIcon: Icon(Icons.search),
|
prefixIcon: const Icon(Icons.search),
|
||||||
border: OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
|
filled: true,
|
||||||
|
fillColor: colors.surfaceContainerHigh,
|
||||||
),
|
),
|
||||||
onSubmitted: (_) => _performSearch(),
|
onSubmitted: (_) => _performSearch(),
|
||||||
),
|
),
|
||||||
|
|
@ -696,7 +760,7 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
backgroundColor: colors.cyberCyan,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
|
|
@ -735,7 +799,11 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
|
|
||||||
if (_searchResult != null) ...[
|
if (_searchResult != null) ...[
|
||||||
Card(
|
Card(
|
||||||
color: Colors.white.withValues(alpha: 0.06),
|
color: colors.surfaceContainerHigh,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(color: colors.outlineVariant),
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|
@ -764,8 +832,8 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
_searchResult!.displayHandle,
|
_searchResult!.displayHandle,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: Color(0xFF00E5FF),
|
color: colors.cyberCyan,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
|
|
@ -796,7 +864,7 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const Divider(height: 24, color: Colors.white12),
|
Divider(height: 24, color: colors.outlineVariant),
|
||||||
|
|
||||||
// Relationship Action
|
// Relationship Action
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -813,32 +881,36 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
)
|
)
|
||||||
else if (_searchResult!.relationship ==
|
else if (_searchResult!.relationship ==
|
||||||
FriendshipStatus.friend)
|
FriendshipStatus.friend)
|
||||||
const Chip(
|
Chip(
|
||||||
avatar: Icon(Icons.check, size: 16),
|
avatar: Icon(Icons.check,
|
||||||
label: Text('Friends'),
|
size: 16, color: colors.cyberCyan),
|
||||||
backgroundColor: Color(0xFF003840),
|
label: const Text('Friends'),
|
||||||
|
backgroundColor:
|
||||||
|
colors.cyberCyan.withValues(alpha: 0.15),
|
||||||
labelStyle: TextStyle(
|
labelStyle: TextStyle(
|
||||||
color: Color(0xFF00E5FF),
|
color: colors.cyberCyan,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else if (_searchResult!.relationship ==
|
else if (_searchResult!.relationship ==
|
||||||
FriendshipStatus.requestOutgoing)
|
FriendshipStatus.requestOutgoing)
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.white12,
|
backgroundColor:
|
||||||
|
colors.surfaceContainerHighest,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.cancel_outlined, size: 16),
|
icon: const Icon(Icons.cancel_outlined,
|
||||||
|
size: 16),
|
||||||
label: const Text('CANCEL REQUEST'),
|
label: const Text('CANCEL REQUEST'),
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
_cancelFriendRequest(_searchResult!.uid),
|
_cancelFriendRequest(_searchResult!.uid),
|
||||||
)
|
)
|
||||||
else if (_searchResult!.relationship ==
|
else if (_searchResult!.relationship ==
|
||||||
FriendshipStatus.requestIncoming)
|
FriendshipStatus.requestIncoming)
|
||||||
const Text(
|
Text(
|
||||||
'Check Requests tab to respond',
|
'Check Requests tab to respond',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.amberAccent,
|
color: colors.warningAmber,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -853,7 +925,7 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
else
|
else
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
backgroundColor: colors.cyberCyan,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.person_add, size: 16),
|
icon: const Icon(Icons.person_add, size: 16),
|
||||||
|
|
@ -891,4 +963,41 @@ class _FriendsScreenState extends ConsumerState<FriendsScreen>
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildFriendMenu(
|
||||||
|
FriendSummary friend, MotorSignalThemeColors colors) {
|
||||||
|
return PopupMenuButton<String>(
|
||||||
|
icon: const Icon(Icons.more_vert, color: Colors.white60),
|
||||||
|
color: colors.surfaceContainerHighest,
|
||||||
|
onSelected: (val) {
|
||||||
|
if (val == 'remove') {
|
||||||
|
_removeFriend(friend);
|
||||||
|
} else if (val == 'block') {
|
||||||
|
_blockRider(friend.uid, friend.handle, friend.displayName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: 'remove',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.person_remove_outlined, size: 18),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Remove Friend'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'block',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.block, size: 18, color: colors.raceRed),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text('Block Rider', style: TextStyle(color: colors.raceRed)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../core/theme/evolution_theme_config.dart';
|
import '../../core/responsive/responsive_layout.dart';
|
||||||
|
import '../../core/responsive/viewport_size.dart';
|
||||||
|
import '../../core/theme/theme_extensions.dart';
|
||||||
import '../../services/auth/auth_providers.dart';
|
import '../../services/auth/auth_providers.dart';
|
||||||
import '../../services/social/handle_validator.dart';
|
import '../../services/social/handle_validator.dart';
|
||||||
import '../../services/social/rider_profile_models.dart';
|
import '../../services/social/rider_profile_models.dart';
|
||||||
|
|
@ -75,11 +77,14 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replaceAll(RegExp(r'[^a-z0-9_]'), '_');
|
.replaceAll(RegExp(r'[^a-z0-9_]'), '_');
|
||||||
if (candidate.length >= 3) {
|
if (candidate.length >= 3) {
|
||||||
_handleController.text = candidate.substring(0, candidate.length.clamp(0, 18));
|
_handleController.text =
|
||||||
|
candidate.substring(0, candidate.length.clamp(0, 18));
|
||||||
}
|
}
|
||||||
} else if (user.email != null) {
|
} else if (user.email != null) {
|
||||||
final candidate = user.email!.split('@').first.replaceAll(RegExp(r'[^a-z0-9_]'), '_');
|
final candidate =
|
||||||
_handleController.text = candidate.substring(0, candidate.length.clamp(0, 18));
|
user.email!.split('@').first.replaceAll(RegExp(r'[^a-z0-9_]'), '_');
|
||||||
|
_handleController.text =
|
||||||
|
candidate.substring(0, candidate.length.clamp(0, 18));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_selectedAvatarPreset = PresetAvatar.availablePresets.first.id;
|
_selectedAvatarPreset = PresetAvatar.availablePresets.first.id;
|
||||||
|
|
@ -142,21 +147,18 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colors = context.motorColors;
|
||||||
final authState = ref.watch(authNotifierProvider);
|
final authState = ref.watch(authNotifierProvider);
|
||||||
final isAuthed = authState.isAuthenticated;
|
final isAuthed = authState.isAuthenticated;
|
||||||
final themeMode = ref.watch(dashboardThemeModeProvider);
|
|
||||||
final isEvolution = themeMode == DashboardThemeMode.evolution;
|
|
||||||
|
|
||||||
final bgColor = isEvolution ? Colors.black : Theme.of(context).scaffoldBackgroundColor;
|
|
||||||
final titleFont = isEvolution ? 'Sora' : null;
|
|
||||||
|
|
||||||
if (!isAuthed) {
|
if (!isAuthed) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: bgColor,
|
backgroundColor: colors.surfaceContainer,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
|
title: const Text(
|
||||||
'Rider Profile',
|
'Rider Profile',
|
||||||
style: TextStyle(fontFamily: titleFont, fontWeight: FontWeight.w800),
|
style: TextStyle(fontWeight: FontWeight.w800),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
|
|
@ -165,16 +167,15 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
Icons.person_pin_circle_outlined,
|
Icons.person_pin_circle_outlined,
|
||||||
size: 64,
|
size: 64,
|
||||||
color: Color(0xFF00E5FF),
|
color: colors.cyberCyan,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
const Text(
|
||||||
'MOTORCLOUD ACCOUNT REQUIRED',
|
'MOTORCLOUD ACCOUNT REQUIRED',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: titleFont,
|
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|
@ -190,7 +191,7 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
backgroundColor: colors.cyberCyan,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
),
|
),
|
||||||
icon: const Icon(Icons.login),
|
icon: const Icon(Icons.login),
|
||||||
|
|
@ -207,11 +208,12 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
|
||||||
final profileAsync = ref.watch(myRiderProfileProvider);
|
final profileAsync = ref.watch(myRiderProfileProvider);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: bgColor,
|
backgroundColor: colors.surfaceContainer,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
backgroundColor: colors.surfaceContainerHigh,
|
||||||
|
title: const Text(
|
||||||
'Rider Profile',
|
'Rider Profile',
|
||||||
style: TextStyle(fontFamily: titleFont, fontWeight: FontWeight.w800),
|
style: TextStyle(fontWeight: FontWeight.w800),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
@ -222,8 +224,15 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: profileAsync.when(
|
body: profileAsync.when(
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => Center(
|
||||||
error: (err, _) => Center(child: Text('Error loading profile: $err')),
|
child: CircularProgressIndicator(color: colors.cyberCyan),
|
||||||
|
),
|
||||||
|
error: (err, _) => Center(
|
||||||
|
child: Text(
|
||||||
|
'Error loading profile: $err',
|
||||||
|
style: TextStyle(color: colors.raceRed),
|
||||||
|
),
|
||||||
|
),
|
||||||
data: (profile) {
|
data: (profile) {
|
||||||
if (profile != null) {
|
if (profile != null) {
|
||||||
_populateFromProfile(profile);
|
_populateFromProfile(profile);
|
||||||
|
|
@ -231,228 +240,439 @@ class _RiderProfileScreenState extends ConsumerState<RiderProfileScreen> {
|
||||||
_populateDefaults();
|
_populateDefaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return ResponsiveLayout(
|
||||||
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 100.0),
|
builder: (context, snapshot) {
|
||||||
child: Form(
|
final isWide = snapshot.sizeClass == WindowSizeClass.expanded ||
|
||||||
key: _formKey,
|
(snapshot.isLandscape && snapshot.width >= 720);
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
// Header avatar preview
|
|
||||||
Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
AvatarBadge(
|
|
||||||
avatarPreset: _selectedAvatarPreset,
|
|
||||||
displayName: _displayNameController.text.isEmpty
|
|
||||||
? 'Rider'
|
|
||||||
: _displayNameController.text,
|
|
||||||
radius: 40,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
_handleController.text.isNotEmpty
|
|
||||||
? '@${_handleController.text}'
|
|
||||||
: '@handle',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Color(0xFF00E5FF),
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
fontSize: 14,
|
|
||||||
fontFamily: 'Inter',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
|
|
||||||
// Messages
|
if (isWide) {
|
||||||
if (_errorMessage != null) ...[
|
return _buildWideProfile(context, colors);
|
||||||
Container(
|
}
|
||||||
padding: const EdgeInsets.all(12),
|
return _buildCompactProfile(context, colors);
|
||||||
decoration: BoxDecoration(
|
},
|
||||||
color: Colors.redAccent.withValues(alpha: 0.15),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: Colors.redAccent),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
_errorMessage!,
|
|
||||||
style: const TextStyle(color: Colors.redAccent, fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
if (_successMessage != null) ...[
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.greenAccent.withValues(alpha: 0.15),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: Colors.greenAccent),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
_successMessage!,
|
|
||||||
style: const TextStyle(color: Colors.greenAccent, fontSize: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Avatar preset selector
|
|
||||||
AvatarSelector(
|
|
||||||
selectedPresetId: _selectedAvatarPreset,
|
|
||||||
onSelected: (presetId) {
|
|
||||||
setState(() {
|
|
||||||
_selectedAvatarPreset = presetId;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Handle input
|
|
||||||
TextFormField(
|
|
||||||
controller: _handleController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Rider Handle',
|
|
||||||
hintText: 'e.g. canyon_runner',
|
|
||||||
prefixText: '@',
|
|
||||||
helperText: 'Unique, 3-20 characters, lowercase letters, numbers, and underscores',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
onChanged: (_) => setState(() {}),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Display Name input
|
|
||||||
TextFormField(
|
|
||||||
controller: _displayNameController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Display Name',
|
|
||||||
hintText: 'e.g. Alex Rivera',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
onChanged: (_) => setState(() {}),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Region input
|
|
||||||
TextFormField(
|
|
||||||
controller: _regionController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Riding Region (Optional)',
|
|
||||||
hintText: 'e.g. Blue Ridge Mountains, NC',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Bio input
|
|
||||||
TextFormField(
|
|
||||||
controller: _bioController,
|
|
||||||
maxLines: 3,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Rider Bio (Optional)',
|
|
||||||
hintText: 'Tell fellow riders about what you ride and your riding style...',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Privacy Settings Card
|
|
||||||
Card(
|
|
||||||
color: Colors.white.withValues(alpha: 0.05),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.shield_outlined, color: Color(0xFF00E5FF), size: 18),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'PRIVACY CONTROLS',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Color(0xFF00E5FF),
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
fontSize: 12,
|
|
||||||
letterSpacing: 0.8,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
SwitchListTile(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
title: const Text('Discoverable by Handle', style: TextStyle(fontSize: 14)),
|
|
||||||
subtitle: const Text(
|
|
||||||
'Allow other riders to find your profile by searching your @handle.',
|
|
||||||
style: TextStyle(fontSize: 11, color: Colors.white60),
|
|
||||||
),
|
|
||||||
value: _isSearchableByHandle,
|
|
||||||
onChanged: (val) => setState(() => _isSearchableByHandle = val),
|
|
||||||
),
|
|
||||||
const Divider(color: Colors.white12),
|
|
||||||
SwitchListTile(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
title: const Text('Public Profile', style: TextStyle(fontSize: 14)),
|
|
||||||
subtitle: const Text(
|
|
||||||
'When off, non-friends only see your handle and avatar, hiding bio and region.',
|
|
||||||
style: TextStyle(fontSize: 11, color: Colors.white60),
|
|
||||||
),
|
|
||||||
value: _isProfilePublic,
|
|
||||||
onChanged: (val) => setState(() => _isProfilePublic = val),
|
|
||||||
),
|
|
||||||
const Divider(color: Colors.white12),
|
|
||||||
SwitchListTile(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
title: const Text('Allow Friend Requests', style: TextStyle(fontSize: 14)),
|
|
||||||
subtitle: const Text(
|
|
||||||
'Allow other riders to send you friend requests.',
|
|
||||||
style: TextStyle(fontSize: 11, color: Colors.white60),
|
|
||||||
),
|
|
||||||
value: _allowFriendRequests,
|
|
||||||
onChanged: (val) => setState(() => _allowFriendRequests = val),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Save Button
|
|
||||||
SizedBox(
|
|
||||||
height: 48,
|
|
||||||
child: ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: const Color(0xFF00E5FF),
|
|
||||||
foregroundColor: Colors.black,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: _isSaving ? null : _handleSave,
|
|
||||||
child: _isSaving
|
|
||||||
? const SizedBox(
|
|
||||||
height: 20,
|
|
||||||
width: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Text(
|
|
||||||
'SAVE PROFILE',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.w900, letterSpacing: 0.8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildWideProfile(
|
||||||
|
BuildContext context, MotorSignalThemeColors colors) {
|
||||||
|
return Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (_errorMessage != null) _buildErrorMessage(colors),
|
||||||
|
if (_successMessage != null) _buildSuccessMessage(colors),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Left Column: Avatar & Handle Identity
|
||||||
|
Expanded(
|
||||||
|
flex: 5,
|
||||||
|
child: Card(
|
||||||
|
color: colors.surfaceContainerHigh,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(color: colors.outlineVariant),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
AvatarBadge(
|
||||||
|
avatarPreset: _selectedAvatarPreset,
|
||||||
|
displayName:
|
||||||
|
_displayNameController.text.isEmpty
|
||||||
|
? 'Rider'
|
||||||
|
: _displayNameController.text,
|
||||||
|
radius: 44,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
_handleController.text.isNotEmpty
|
||||||
|
? '@${_handleController.text}'
|
||||||
|
: '@handle',
|
||||||
|
style: TextStyle(
|
||||||
|
color: colors.cyberCyan,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
AvatarSelector(
|
||||||
|
selectedPresetId: _selectedAvatarPreset,
|
||||||
|
onSelected: (presetId) {
|
||||||
|
setState(() {
|
||||||
|
_selectedAvatarPreset = presetId;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
TextFormField(
|
||||||
|
controller: _handleController,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Rider Handle',
|
||||||
|
hintText: 'e.g. canyon_runner',
|
||||||
|
prefixText: '@',
|
||||||
|
helperText:
|
||||||
|
'Unique, 3-20 chars: letters, numbers, underscores',
|
||||||
|
helperStyle: const TextStyle(
|
||||||
|
color: Colors.white38, fontSize: 11),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
filled: true,
|
||||||
|
fillColor: colors.surfaceContainer,
|
||||||
|
),
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _displayNameController,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Display Name',
|
||||||
|
hintText: 'e.g. Alex Rivera',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
filled: true,
|
||||||
|
fillColor: colors.surfaceContainer,
|
||||||
|
),
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
|
||||||
|
// Right Column: Bio, Region & Privacy
|
||||||
|
Expanded(
|
||||||
|
flex: 6,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Card(
|
||||||
|
color: colors.surfaceContainerHigh,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(color: colors.outlineVariant),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _regionController,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Riding Region (Optional)',
|
||||||
|
hintText: 'e.g. Blue Ridge Mountains, NC',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
filled: true,
|
||||||
|
fillColor: colors.surfaceContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _bioController,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
maxLines: 3,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Rider Bio (Optional)',
|
||||||
|
hintText:
|
||||||
|
'Tell fellow riders about your bike and riding style...',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
filled: true,
|
||||||
|
fillColor: colors.surfaceContainer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildPrivacyCard(colors),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildSaveButton(colors),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCompactProfile(
|
||||||
|
BuildContext context, MotorSignalThemeColors colors) {
|
||||||
|
return Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 48.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// Header avatar preview
|
||||||
|
Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
AvatarBadge(
|
||||||
|
avatarPreset: _selectedAvatarPreset,
|
||||||
|
displayName: _displayNameController.text.isEmpty
|
||||||
|
? 'Rider'
|
||||||
|
: _displayNameController.text,
|
||||||
|
radius: 40,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
_handleController.text.isNotEmpty
|
||||||
|
? '@${_handleController.text}'
|
||||||
|
: '@handle',
|
||||||
|
style: TextStyle(
|
||||||
|
color: colors.cyberCyan,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
if (_errorMessage != null) _buildErrorMessage(colors),
|
||||||
|
if (_successMessage != null) _buildSuccessMessage(colors),
|
||||||
|
|
||||||
|
AvatarSelector(
|
||||||
|
selectedPresetId: _selectedAvatarPreset,
|
||||||
|
onSelected: (presetId) {
|
||||||
|
setState(() {
|
||||||
|
_selectedAvatarPreset = presetId;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
TextFormField(
|
||||||
|
controller: _handleController,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Rider Handle',
|
||||||
|
hintText: 'e.g. canyon_runner',
|
||||||
|
prefixText: '@',
|
||||||
|
helperText: 'Unique, 3-20 characters: letters, numbers, underscores',
|
||||||
|
helperStyle:
|
||||||
|
const TextStyle(color: Colors.white38, fontSize: 11),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
filled: true,
|
||||||
|
fillColor: colors.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
TextFormField(
|
||||||
|
controller: _displayNameController,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Display Name',
|
||||||
|
hintText: 'e.g. Alex Rivera',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
filled: true,
|
||||||
|
fillColor: colors.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
onChanged: (_) => setState(() {}),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
TextFormField(
|
||||||
|
controller: _regionController,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Riding Region (Optional)',
|
||||||
|
hintText: 'e.g. Blue Ridge Mountains, NC',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
filled: true,
|
||||||
|
fillColor: colors.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
TextFormField(
|
||||||
|
controller: _bioController,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
maxLines: 3,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Rider Bio (Optional)',
|
||||||
|
hintText:
|
||||||
|
'Tell fellow riders about what you ride and your riding style...',
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
filled: true,
|
||||||
|
fillColor: colors.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
_buildPrivacyCard(colors),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
_buildSaveButton(colors),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildErrorMessage(MotorSignalThemeColors colors) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.raceRed.withValues(alpha: 0.15),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: colors.raceRed),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: TextStyle(color: colors.raceRed, fontSize: 12),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSuccessMessage(MotorSignalThemeColors colors) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colors.launchGreen.withValues(alpha: 0.15),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: colors.launchGreen),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_successMessage!,
|
||||||
|
style: TextStyle(color: colors.launchGreen, fontSize: 12),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPrivacyCard(MotorSignalThemeColors colors) {
|
||||||
|
return Card(
|
||||||
|
color: colors.surfaceContainerHigh,
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(color: colors.outlineVariant),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.shield_outlined,
|
||||||
|
color: colors.cyberCyan, size: 18),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'PRIVACY CONTROLS',
|
||||||
|
style: TextStyle(
|
||||||
|
color: colors.cyberCyan,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 12,
|
||||||
|
letterSpacing: 0.8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
activeThumbColor: colors.cyberCyan,
|
||||||
|
title: const Text('Discoverable by Handle',
|
||||||
|
style: TextStyle(fontSize: 14)),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Allow other riders to find your profile by searching your @handle.',
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.white60),
|
||||||
|
),
|
||||||
|
value: _isSearchableByHandle,
|
||||||
|
onChanged: (val) =>
|
||||||
|
setState(() => _isSearchableByHandle = val),
|
||||||
|
),
|
||||||
|
Divider(color: colors.outlineVariant),
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
activeThumbColor: colors.cyberCyan,
|
||||||
|
title: const Text('Public Profile',
|
||||||
|
style: TextStyle(fontSize: 14)),
|
||||||
|
subtitle: const Text(
|
||||||
|
'When off, non-friends only see your handle and avatar, hiding bio and region.',
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.white60),
|
||||||
|
),
|
||||||
|
value: _isProfilePublic,
|
||||||
|
onChanged: (val) =>
|
||||||
|
setState(() => _isProfilePublic = val),
|
||||||
|
),
|
||||||
|
Divider(color: colors.outlineVariant),
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
activeThumbColor: colors.cyberCyan,
|
||||||
|
title: const Text('Allow Friend Requests',
|
||||||
|
style: TextStyle(fontSize: 14)),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Allow other riders to send you friend requests.',
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.white60),
|
||||||
|
),
|
||||||
|
value: _allowFriendRequests,
|
||||||
|
onChanged: (val) =>
|
||||||
|
setState(() => _allowFriendRequests = val),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSaveButton(MotorSignalThemeColors colors) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: colors.cyberCyan,
|
||||||
|
foregroundColor: Colors.black,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: _isSaving ? null : _handleSave,
|
||||||
|
child: _isSaving
|
||||||
|
? const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Text(
|
||||||
|
'SAVE PROFILE',
|
||||||
|
style:
|
||||||
|
TextStyle(fontWeight: FontWeight.w900, letterSpacing: 0.8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ class AvatarBadge extends StatelessWidget {
|
||||||
padding: const EdgeInsets.all(3),
|
padding: const EdgeInsets.all(3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(color: const Color(0xFF00E5FF), width: 2),
|
border: Border.all(color: Theme.of(context).colorScheme.primary, width: 2),
|
||||||
),
|
),
|
||||||
child: inner,
|
child: inner,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,539 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:motorcycle_dash/core/theme/theme.dart';
|
||||||
|
import 'package:motorcycle_dash/features/group_rides/group_rides_hub_screen.dart';
|
||||||
|
import 'package:motorcycle_dash/features/group_rides/create_group_ride_screen.dart';
|
||||||
|
import 'package:motorcycle_dash/features/group_rides/group_ride_detail_screen.dart';
|
||||||
|
import 'package:motorcycle_dash/features/group_rides/live_group_ride_screen.dart';
|
||||||
|
import 'package:motorcycle_dash/features/social/rider_profile_screen.dart';
|
||||||
|
import 'package:motorcycle_dash/features/social/friends_screen.dart';
|
||||||
|
import 'package:motorcycle_dash/services/auth/auth_providers.dart';
|
||||||
|
import 'package:motorcycle_dash/services/auth/auth_service.dart';
|
||||||
|
import 'package:motorcycle_dash/services/auth/auth_state.dart';
|
||||||
|
import 'package:motorcycle_dash/services/auth/auth_user.dart';
|
||||||
|
import 'package:motorcycle_dash/services/group_rides/group_ride_models.dart';
|
||||||
|
import 'package:motorcycle_dash/services/group_rides/group_ride_providers.dart';
|
||||||
|
import 'package:motorcycle_dash/services/group_rides/live_broadcast_controller.dart';
|
||||||
|
import 'package:motorcycle_dash/services/social/rider_profile_models.dart';
|
||||||
|
import 'package:motorcycle_dash/services/social/social_providers.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:motorcycle_dash/features/records/records_service.dart';
|
||||||
|
|
||||||
|
class _FakeAuth implements AuthService {
|
||||||
|
final _states = StreamController<AuthState>.broadcast();
|
||||||
|
AuthUser? user;
|
||||||
|
|
||||||
|
_FakeAuth({this.user});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<AuthState> get authStateChanges => _states.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
AuthState get currentState => user == null
|
||||||
|
? const AuthState.unauthenticated(isFirebaseConfigured: true)
|
||||||
|
: (user!.isAnonymous
|
||||||
|
? AuthState.anonymous(user!, isFirebaseConfigured: true)
|
||||||
|
: AuthState.authenticated(user!, isFirebaseConfigured: true));
|
||||||
|
|
||||||
|
@override
|
||||||
|
AuthUser? get currentUser => user;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isConfigured => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isEmailVerified => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteCurrentUser() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() => _states.close();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> getIdToken([bool forceRefresh = false]) async => 'token';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> reauthenticate({String? password}) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> reloadCurrentUser() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> sendEmailVerification() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> sendPasswordResetEmail(String email) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> signInAnonymously() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> signInWithEmailAndPassword(String email, String password) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> createUserWithEmailAndPassword(String email, String password,
|
||||||
|
{String? displayName}) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> signInWithGoogle() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> signOut() async {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FakeLiveBroadcastController extends StateNotifier<LiveBroadcastState>
|
||||||
|
implements LiveBroadcastController {
|
||||||
|
_FakeLiveBroadcastController(super.initialState);
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setOptIn(bool optIn) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setStatus(LivePresenceStatus status) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> triggerRegroup() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clearRegroup() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateAppLifecycle(AppLifecycleState state) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
late SharedPreferences prefs;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
prefs = await SharedPreferences.getInstance();
|
||||||
|
});
|
||||||
|
|
||||||
|
const testUser = AuthUser(
|
||||||
|
uid: 'uid_lead',
|
||||||
|
email: 'rider@motorsignal.app',
|
||||||
|
displayName: 'Canyon Lead',
|
||||||
|
isAnonymous: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final testProfile = RiderProfile(
|
||||||
|
uid: 'uid_lead',
|
||||||
|
handle: 'canyon_lead',
|
||||||
|
displayName: 'Canyon Lead',
|
||||||
|
bio: 'Twisty roads and track days',
|
||||||
|
region: 'Blue Ridge, NC',
|
||||||
|
avatarPreset: 'preset_neon_cyan',
|
||||||
|
privacySettings: const ProfilePrivacySettings(
|
||||||
|
isSearchableByHandle: true,
|
||||||
|
isProfilePublic: true,
|
||||||
|
allowFriendRequests: true,
|
||||||
|
),
|
||||||
|
createdAt: DateTime(2026, 1, 1),
|
||||||
|
updatedAt: DateTime(2026, 1, 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
final now = DateTime(2026, 9, 1, 12, 0);
|
||||||
|
|
||||||
|
final testRide = GroupRide(
|
||||||
|
id: 'ride_101',
|
||||||
|
organizerUid: 'uid_lead',
|
||||||
|
organizerHandle: 'canyon_lead',
|
||||||
|
organizerDisplayName: 'Canyon Lead',
|
||||||
|
title: 'Blue Ridge Twisties Run',
|
||||||
|
description: 'Scenic group ride through Blue Ridge twisties.',
|
||||||
|
scheduledStartAt: DateTime(2026, 10, 15, 9, 0),
|
||||||
|
departureAt: DateTime(2026, 10, 15, 9, 30),
|
||||||
|
status: GroupRideStatus.underway,
|
||||||
|
joinPolicy: GroupRideJoinPolicy.open,
|
||||||
|
visibility: GroupRideVisibility.public,
|
||||||
|
meetingPoint: const GroupRideLocation(
|
||||||
|
latitude: 35.5951,
|
||||||
|
longitude: -82.5515,
|
||||||
|
displayName: 'Asheville Meetup',
|
||||||
|
locationType: GroupRideLocationType.meetup,
|
||||||
|
),
|
||||||
|
destination: const GroupRideLocation(
|
||||||
|
latitude: 35.7951,
|
||||||
|
longitude: -82.2515,
|
||||||
|
displayName: 'Mount Mitchell Peak',
|
||||||
|
locationType: GroupRideLocationType.destination,
|
||||||
|
),
|
||||||
|
participantCount: 3,
|
||||||
|
stops: const [
|
||||||
|
GroupRideLocation(
|
||||||
|
latitude: 35.65,
|
||||||
|
longitude: -82.40,
|
||||||
|
displayName: 'Scenic Overlook Stop',
|
||||||
|
locationType: GroupRideLocationType.scenic,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<GroupRideParticipant> testParticipants = [
|
||||||
|
GroupRideParticipant(
|
||||||
|
userId: 'uid_lead',
|
||||||
|
riderHandle: 'canyon_lead',
|
||||||
|
riderDisplayName: 'Canyon Lead',
|
||||||
|
role: GroupRideRole.lead,
|
||||||
|
status: ParticipantStatus.accepted,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
),
|
||||||
|
GroupRideParticipant(
|
||||||
|
userId: 'uid_sweep',
|
||||||
|
riderHandle: 'tail_gunner',
|
||||||
|
riderDisplayName: 'Tail Gunner',
|
||||||
|
role: GroupRideRole.sweep,
|
||||||
|
status: ParticipantStatus.accepted,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
),
|
||||||
|
GroupRideParticipant(
|
||||||
|
userId: 'uid_rider3',
|
||||||
|
riderHandle: 'moto_enthusiast',
|
||||||
|
riderDisplayName: 'Moto Enthusiast',
|
||||||
|
role: GroupRideRole.participant,
|
||||||
|
status: ParticipantStatus.accepted,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<LiveGroupPresence> testPresences = [
|
||||||
|
LiveGroupPresence(
|
||||||
|
userId: 'uid_lead',
|
||||||
|
groupRideId: 'ride_101',
|
||||||
|
latitude: 35.5951,
|
||||||
|
longitude: -82.5515,
|
||||||
|
heading: 45.0,
|
||||||
|
speedMph: 45.0,
|
||||||
|
accuracyMeters: 5.0,
|
||||||
|
status: LivePresenceStatus.riding,
|
||||||
|
sharingEnabled: true,
|
||||||
|
updatedAt: now,
|
||||||
|
expiresAt: now.add(const Duration(seconds: 120)),
|
||||||
|
),
|
||||||
|
LiveGroupPresence(
|
||||||
|
userId: 'uid_sweep',
|
||||||
|
groupRideId: 'ride_101',
|
||||||
|
latitude: 35.5900,
|
||||||
|
longitude: -82.5500,
|
||||||
|
heading: 40.0,
|
||||||
|
speedMph: 42.0,
|
||||||
|
accuracyMeters: 5.0,
|
||||||
|
status: LivePresenceStatus.riding,
|
||||||
|
sharingEnabled: true,
|
||||||
|
updatedAt: now.subtract(const Duration(seconds: 8)),
|
||||||
|
expiresAt: now.add(const Duration(seconds: 112)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<FriendSummary> testFriends = [
|
||||||
|
FriendSummary(
|
||||||
|
uid: 'uid_sweep',
|
||||||
|
handle: 'tail_gunner',
|
||||||
|
displayName: 'Tail Gunner',
|
||||||
|
avatarPreset: 'preset_amber_flame',
|
||||||
|
friendedAt: DateTime(2026, 8, 1),
|
||||||
|
),
|
||||||
|
FriendSummary(
|
||||||
|
uid: 'uid_rider3',
|
||||||
|
handle: 'moto_enthusiast',
|
||||||
|
displayName: 'Moto Enthusiast',
|
||||||
|
avatarPreset: 'preset_electric_green',
|
||||||
|
friendedAt: DateTime(2026, 8, 10),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final List<FriendRequest> testIncomingRequests = [
|
||||||
|
FriendRequest(
|
||||||
|
requestId: 'req_1',
|
||||||
|
senderUid: 'uid_guest',
|
||||||
|
senderHandle: 'speed_demon',
|
||||||
|
senderDisplayName: 'Speed Demon',
|
||||||
|
senderAvatarPreset: 'preset_hyper_purple',
|
||||||
|
receiverUid: 'uid_lead',
|
||||||
|
sentAt: DateTime(2026, 9, 4),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
final viewports = <String, Size>{
|
||||||
|
'Compact Phone (320x640)': const Size(320, 640),
|
||||||
|
'Standard Phone (400x800)': const Size(400, 800),
|
||||||
|
'Tall Phone (412x915)': const Size(412, 915),
|
||||||
|
'Small Landscape (640x320)': const Size(640, 320),
|
||||||
|
'Short Moto Mount Landscape (780x360)': const Size(780, 360),
|
||||||
|
'Standard Landscape (800x360)': const Size(800, 360),
|
||||||
|
'Wide Landscape (840x390)': const Size(840, 390),
|
||||||
|
'Medium Tablet Portrait (768x1024)': const Size(768, 1024),
|
||||||
|
'Expanded Tablet Landscape (1024x768)': const Size(1024, 768),
|
||||||
|
};
|
||||||
|
|
||||||
|
group('GroupRidesHubScreen Responsive Viewport Matrix', () {
|
||||||
|
for (final entry in viewports.entries) {
|
||||||
|
testWidgets('Renders cleanly without overflow on ${entry.key}',
|
||||||
|
(tester) async {
|
||||||
|
tester.view.physicalSize = entry.value;
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
|
||||||
|
final fakeAuth = _FakeAuth(user: testUser);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
||||||
|
authServiceProvider.overrideWithValue(fakeAuth),
|
||||||
|
currentUserProvider.overrideWithValue(testUser),
|
||||||
|
myRiderProfileProvider.overrideWith(
|
||||||
|
(ref) => Stream.value(testProfile),
|
||||||
|
),
|
||||||
|
upcomingGroupRidesProvider.overrideWith(
|
||||||
|
(ref) => Stream.value([testRide]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: AppTheme.darkTheme,
|
||||||
|
home: const GroupRidesHubScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('GROUP RIDES'), findsOneWidget);
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
group('CreateGroupRideScreen Responsive Matrix', () {
|
||||||
|
for (final entry in [
|
||||||
|
const MapEntry('Compact (400x800)', Size(400, 800)),
|
||||||
|
const MapEntry('Short Landscape (800x360)', Size(800, 360)),
|
||||||
|
const MapEntry('Expanded Tablet (1024x768)', Size(1024, 768)),
|
||||||
|
]) {
|
||||||
|
testWidgets('Renders cleanly on ${entry.key}', (tester) async {
|
||||||
|
tester.view.physicalSize = entry.value;
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
|
||||||
|
final fakeAuth = _FakeAuth(user: testUser);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
||||||
|
authServiceProvider.overrideWithValue(fakeAuth),
|
||||||
|
currentUserProvider.overrideWithValue(testUser),
|
||||||
|
myRiderProfileProvider.overrideWith(
|
||||||
|
(ref) => Stream.value(testProfile),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: AppTheme.darkTheme,
|
||||||
|
home: const CreateGroupRideScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('CREATE GROUP RIDE'), findsOneWidget);
|
||||||
|
expect(find.text('1. RIDE DETAILS'), findsOneWidget);
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
group('GroupRideDetailScreen Responsive Matrix', () {
|
||||||
|
for (final entry in [
|
||||||
|
const MapEntry('Compact (400x800)', Size(400, 800)),
|
||||||
|
const MapEntry('Short Landscape (800x360)', Size(800, 360)),
|
||||||
|
const MapEntry('Expanded Tablet (1024x768)', Size(1024, 768)),
|
||||||
|
]) {
|
||||||
|
testWidgets('Renders 2-column on wide and details cleanly on ${entry.key}',
|
||||||
|
(tester) async {
|
||||||
|
tester.view.physicalSize = entry.value;
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
|
||||||
|
final fakeAuth = _FakeAuth(user: testUser);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
||||||
|
authServiceProvider.overrideWithValue(fakeAuth),
|
||||||
|
currentUserProvider.overrideWithValue(testUser),
|
||||||
|
groupRideStreamProvider('ride_101').overrideWith(
|
||||||
|
(ref) => Stream.value(testRide),
|
||||||
|
),
|
||||||
|
groupRideParticipantsProvider('ride_101').overrideWith(
|
||||||
|
(ref) => Stream.value(testParticipants),
|
||||||
|
),
|
||||||
|
groupRideLivePresenceProvider('ride_101').overrideWith(
|
||||||
|
(ref) => Stream.value(testPresences),
|
||||||
|
),
|
||||||
|
myRiderProfileProvider.overrideWith(
|
||||||
|
(ref) => Stream.value(testProfile),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: AppTheme.darkTheme,
|
||||||
|
home: const GroupRideDetailScreen(rideId: 'ride_101'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Blue Ridge Twisties Run'), findsOneWidget);
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
group('LiveGroupRideScreen Responsive Matrix', () {
|
||||||
|
for (final entry in [
|
||||||
|
const MapEntry('Compact (400x800)', Size(400, 800)),
|
||||||
|
const MapEntry('Short Landscape (800x360)', Size(800, 360)),
|
||||||
|
const MapEntry('Expanded Tablet (1024x768)', Size(1024, 768)),
|
||||||
|
]) {
|
||||||
|
testWidgets('Renders map, status bar, and controls on ${entry.key}',
|
||||||
|
(tester) async {
|
||||||
|
tester.view.physicalSize = entry.value;
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
|
||||||
|
final fakeAuth = _FakeAuth(user: testUser);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
||||||
|
authServiceProvider.overrideWithValue(fakeAuth),
|
||||||
|
currentUserProvider.overrideWithValue(testUser),
|
||||||
|
groupRideStreamProvider('ride_101').overrideWith(
|
||||||
|
(ref) => Stream.value(testRide),
|
||||||
|
),
|
||||||
|
groupRideParticipantsProvider('ride_101').overrideWith(
|
||||||
|
(ref) => Stream.value(testParticipants),
|
||||||
|
),
|
||||||
|
groupRideLivePresenceProvider('ride_101').overrideWith(
|
||||||
|
(ref) => Stream.value(testPresences),
|
||||||
|
),
|
||||||
|
liveBroadcastControllerProvider('ride_101').overrideWith(
|
||||||
|
(ref) => _FakeLiveBroadcastController(
|
||||||
|
const LiveBroadcastState(
|
||||||
|
isOptedIn: true,
|
||||||
|
isBroadcasting: true,
|
||||||
|
status: LivePresenceStatus.riding,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: AppTheme.darkTheme,
|
||||||
|
home: const LiveGroupRideScreen(rideId: 'ride_101'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(const ValueKey('live_ride_broadcast_switch')),
|
||||||
|
findsOneWidget);
|
||||||
|
expect(find.byKey(const ValueKey('live_ride_recenter_button')),
|
||||||
|
findsOneWidget);
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
group('RiderProfileScreen & FriendsScreen Responsive Matrix', () {
|
||||||
|
for (final entry in [
|
||||||
|
const MapEntry('Compact (400x800)', Size(400, 800)),
|
||||||
|
const MapEntry('Wide (1024x768)', Size(1024, 768)),
|
||||||
|
]) {
|
||||||
|
testWidgets('RiderProfileScreen renders on ${entry.key}', (tester) async {
|
||||||
|
tester.view.physicalSize = entry.value;
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
|
||||||
|
final fakeAuth = _FakeAuth(user: testUser);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
||||||
|
authServiceProvider.overrideWithValue(fakeAuth),
|
||||||
|
currentUserProvider.overrideWithValue(testUser),
|
||||||
|
myRiderProfileProvider.overrideWith(
|
||||||
|
(ref) => Stream.value(testProfile),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: AppTheme.darkTheme,
|
||||||
|
home: const RiderProfileScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Rider Profile'), findsOneWidget);
|
||||||
|
expect(find.text('PRIVACY CONTROLS'), findsOneWidget);
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('FriendsScreen renders on ${entry.key}', (tester) async {
|
||||||
|
tester.view.physicalSize = entry.value;
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.resetPhysicalSize);
|
||||||
|
|
||||||
|
final fakeAuth = _FakeAuth(user: testUser);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
sharedPreferencesProvider.overrideWithValue(prefs),
|
||||||
|
authServiceProvider.overrideWithValue(fakeAuth),
|
||||||
|
currentUserProvider.overrideWithValue(testUser),
|
||||||
|
myRiderProfileProvider.overrideWith(
|
||||||
|
(ref) => Stream.value(testProfile),
|
||||||
|
),
|
||||||
|
myFriendsProvider.overrideWith(
|
||||||
|
(ref) => Stream.value(testFriends),
|
||||||
|
),
|
||||||
|
myIncomingRequestsProvider.overrideWith(
|
||||||
|
(ref) => Stream.value(testIncomingRequests),
|
||||||
|
),
|
||||||
|
myOutgoingRequestsProvider.overrideWith(
|
||||||
|
(ref) => Stream.value(<FriendRequest>[]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: AppTheme.darkTheme,
|
||||||
|
home: const FriendsScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Friends & Riders'), findsOneWidget);
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue