fix(group-rides): address PR #27 review findings and responsive dashboard layout

This commit is contained in:
Developer 2026-09-25 15:35:32 -04:00
parent 67c25b5c50
commit 30539108fa
5 changed files with 376 additions and 180 deletions

View File

@ -148,6 +148,10 @@ class ResponsiveDashboardLayout extends ConsumerWidget {
),
SizedBox(height: isShortPortrait ? 6 : 16),
RideActionButton(compact: isShortPortrait),
if (!isShortPortrait) ...[
const SizedBox(height: 6),
const PerformanceLaunchShortcut(compact: true),
],
] else if (mode == DashboardLayout.touring) ...[
SizedBox(height: isShortPortrait ? 2 : 6),
SpeedReadout(
@ -211,6 +215,10 @@ class ResponsiveDashboardLayout extends ConsumerWidget {
),
SizedBox(height: isShortPortrait ? 4 : 10),
RideActionButton(compact: isShortPortrait),
if (!isShortPortrait) ...[
const SizedBox(height: 6),
const PerformanceLaunchShortcut(compact: true),
],
] else ...[
// Sport Cockpit Mode
if (!isShortPortrait) const Spacer(flex: 1),

File diff suppressed because it is too large Load Diff

View File

@ -583,7 +583,7 @@ class GroupRideService {
final response = await httpClient.get(
uri,
headers: {
'Authorization': 'Bearer $idToken',
'Authorization': 'Bearer $token',
'Accept': 'application/json',
},
).timeout(const Duration(seconds: 15));

View File

@ -75,6 +75,8 @@ class LiveBroadcastState {
/// Implements dual-gate privacy (global + per-ride), 5s moving / 30s stopped heartbeat,
/// 30m accuracy filtering, and immediate termination on authorization loss.
class LiveBroadcastController extends StateNotifier<LiveBroadcastState> {
static final Set<String> _persistedOptedInRideIds = <String>{};
final String rideId;
final String callerUid;
final GroupRideService service;
@ -107,9 +109,13 @@ class LiveBroadcastController extends StateNotifier<LiveBroadcastState> {
this.participantsStream,
this.presenceStream,
bool initialMasterAllowed = false,
bool? initialOptIn,
DateTime Function()? now,
}) : _now = now ?? DateTime.now,
super(LiveBroadcastState(isMasterAllowed: initialMasterAllowed)) {
super(LiveBroadcastState(
isMasterAllowed: initialMasterAllowed,
isOptedIn: initialOptIn ?? _persistedOptedInRideIds.contains(rideId),
)) {
_initSubscriptions();
}
@ -151,12 +157,11 @@ class LiveBroadcastController extends StateNotifier<LiveBroadcastState> {
return;
}
// Ride must be assembling or underway
final isRideActive = ride.status == GroupRideStatus.assembling ||
ride.status == GroupRideStatus.underway;
// Ride must be live-eligible (scheduled, assembling, or underway)
final isRideActive = ride.isLiveEligible;
if (!isRideActive) {
// Completed, canceled, or scheduled -> stop broadcast
// Completed or canceled -> stop broadcast
_revokeBroadcasting('Ride is not in active state (${ride.status.name})');
return;
}
@ -232,14 +237,49 @@ class LiveBroadcastController extends StateNotifier<LiveBroadcastState> {
_evaluateEligibility();
if (!optIn) {
_persistedOptedInRideIds.remove(rideId);
// Immediately clear presence document in Firestore
await service.clearLivePresence(
callerUid: callerUid,
groupRideId: rideId,
);
} else if (state.isBroadcasting && state.lastPosition != null) {
// Force immediate publish on opt-in
await _publishPosition(state.lastPosition!, force: true);
} else {
_persistedOptedInRideIds.add(rideId);
if (state.isBroadcasting) {
if (state.lastPosition != null) {
// Force immediate publish on opt-in
await _publishPosition(state.lastPosition!, force: true);
} else {
await refreshAndPublishCurrentLocation();
}
}
}
}
/// Forces an immediate location sample and presence publish if broadcasting.
Future<void> refreshAndPublishCurrentLocation() async {
if (_isDisposed) return;
try {
Position? pos;
try {
pos = await Geolocator.getLastKnownPosition();
pos ??= await Geolocator.getCurrentPosition(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high,
timeLimit: Duration(seconds: 4),
),
);
} catch (_) {
// Fallback or ignore in headless test environments
}
if (pos != null && !_isDisposed) {
_onPositionUpdate(pos);
if (state.isBroadcasting) {
await _publishPosition(pos, force: true);
}
}
} catch (e) {
debugPrint('[LiveBroadcastController] refreshAndPublishCurrentLocation error: $e');
}
}
@ -446,6 +486,7 @@ class LiveBroadcastController extends StateNotifier<LiveBroadcastState> {
}
void _revokeBroadcasting(String reason) {
_persistedOptedInRideIds.remove(rideId);
state = state.copyWith(
isEligible: false,
isBroadcasting: false,
@ -481,7 +522,7 @@ class LiveBroadcastController extends StateNotifier<LiveBroadcastState> {
}
/// Provider for LiveBroadcastController family parameterized by group ride ID.
final liveBroadcastControllerProvider = StateNotifierProvider.autoDispose
final liveBroadcastControllerProvider = StateNotifierProvider
.family<LiveBroadcastController, LiveBroadcastState, String>(
(ref, rideId) {
final user = ref.watch(currentUserProvider);

View File

@ -84,14 +84,50 @@ class _RecordingGroupRideService extends Fake implements GroupRideService {
Future<void> respondToJoinRequest({
required String callerUid,
required String groupRideId,
required String requesterUid,
required bool approve,
required String requesterUid,
}) async {
lastModeratedCallerUid = callerUid;
lastModeratedGroupRideId = groupRideId;
lastModeratedRequesterUid = requesterUid;
lastModeratedApprove = approve;
}
@override
Stream<GroupRideParticipant?> participantDocStream({
required String groupRideId,
required String participantUid,
}) =>
Stream.value(null);
@override
Stream<GroupRide?> groupRideStream(String groupRideId) => Stream.value(null);
@override
Stream<LiveGroupPresence?> livePresenceDocStream({
required String groupRideId,
required String targetUid,
}) =>
Stream.value(null);
@override
Future<void> publishLivePresence({
required String callerUid,
required String groupRideId,
required double latitude,
required double longitude,
double? heading,
double? speedMph,
double? accuracyMeters,
LivePresenceStatus status = LivePresenceStatus.riding,
bool sharingEnabled = true,
}) async {}
@override
Future<void> clearLivePresence({
required String callerUid,
required String groupRideId,
}) async {}
}
void main() {