fix(group-rides): address PR #27 review findings and responsive dashboard layout
This commit is contained in:
parent
67c25b5c50
commit
30539108fa
|
|
@ -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
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue