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),
|
SizedBox(height: isShortPortrait ? 6 : 16),
|
||||||
RideActionButton(compact: isShortPortrait),
|
RideActionButton(compact: isShortPortrait),
|
||||||
|
if (!isShortPortrait) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
const PerformanceLaunchShortcut(compact: true),
|
||||||
|
],
|
||||||
] else if (mode == DashboardLayout.touring) ...[
|
] else if (mode == DashboardLayout.touring) ...[
|
||||||
SizedBox(height: isShortPortrait ? 2 : 6),
|
SizedBox(height: isShortPortrait ? 2 : 6),
|
||||||
SpeedReadout(
|
SpeedReadout(
|
||||||
|
|
@ -211,6 +215,10 @@ class ResponsiveDashboardLayout extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
SizedBox(height: isShortPortrait ? 4 : 10),
|
SizedBox(height: isShortPortrait ? 4 : 10),
|
||||||
RideActionButton(compact: isShortPortrait),
|
RideActionButton(compact: isShortPortrait),
|
||||||
|
if (!isShortPortrait) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
const PerformanceLaunchShortcut(compact: true),
|
||||||
|
],
|
||||||
] else ...[
|
] else ...[
|
||||||
// Sport Cockpit Mode
|
// Sport Cockpit Mode
|
||||||
if (!isShortPortrait) const Spacer(flex: 1),
|
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(
|
final response = await httpClient.get(
|
||||||
uri,
|
uri,
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': 'Bearer $idToken',
|
'Authorization': 'Bearer $token',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
).timeout(const Duration(seconds: 15));
|
).timeout(const Duration(seconds: 15));
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,8 @@ class LiveBroadcastState {
|
||||||
/// Implements dual-gate privacy (global + per-ride), 5s moving / 30s stopped heartbeat,
|
/// Implements dual-gate privacy (global + per-ride), 5s moving / 30s stopped heartbeat,
|
||||||
/// 30m accuracy filtering, and immediate termination on authorization loss.
|
/// 30m accuracy filtering, and immediate termination on authorization loss.
|
||||||
class LiveBroadcastController extends StateNotifier<LiveBroadcastState> {
|
class LiveBroadcastController extends StateNotifier<LiveBroadcastState> {
|
||||||
|
static final Set<String> _persistedOptedInRideIds = <String>{};
|
||||||
|
|
||||||
final String rideId;
|
final String rideId;
|
||||||
final String callerUid;
|
final String callerUid;
|
||||||
final GroupRideService service;
|
final GroupRideService service;
|
||||||
|
|
@ -107,9 +109,13 @@ class LiveBroadcastController extends StateNotifier<LiveBroadcastState> {
|
||||||
this.participantsStream,
|
this.participantsStream,
|
||||||
this.presenceStream,
|
this.presenceStream,
|
||||||
bool initialMasterAllowed = false,
|
bool initialMasterAllowed = false,
|
||||||
|
bool? initialOptIn,
|
||||||
DateTime Function()? now,
|
DateTime Function()? now,
|
||||||
}) : _now = now ?? DateTime.now,
|
}) : _now = now ?? DateTime.now,
|
||||||
super(LiveBroadcastState(isMasterAllowed: initialMasterAllowed)) {
|
super(LiveBroadcastState(
|
||||||
|
isMasterAllowed: initialMasterAllowed,
|
||||||
|
isOptedIn: initialOptIn ?? _persistedOptedInRideIds.contains(rideId),
|
||||||
|
)) {
|
||||||
_initSubscriptions();
|
_initSubscriptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,12 +157,11 @@ class LiveBroadcastController extends StateNotifier<LiveBroadcastState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ride must be assembling or underway
|
// Ride must be live-eligible (scheduled, assembling, or underway)
|
||||||
final isRideActive = ride.status == GroupRideStatus.assembling ||
|
final isRideActive = ride.isLiveEligible;
|
||||||
ride.status == GroupRideStatus.underway;
|
|
||||||
|
|
||||||
if (!isRideActive) {
|
if (!isRideActive) {
|
||||||
// Completed, canceled, or scheduled -> stop broadcast
|
// Completed or canceled -> stop broadcast
|
||||||
_revokeBroadcasting('Ride is not in active state (${ride.status.name})');
|
_revokeBroadcasting('Ride is not in active state (${ride.status.name})');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -232,14 +237,49 @@ class LiveBroadcastController extends StateNotifier<LiveBroadcastState> {
|
||||||
_evaluateEligibility();
|
_evaluateEligibility();
|
||||||
|
|
||||||
if (!optIn) {
|
if (!optIn) {
|
||||||
|
_persistedOptedInRideIds.remove(rideId);
|
||||||
// Immediately clear presence document in Firestore
|
// Immediately clear presence document in Firestore
|
||||||
await service.clearLivePresence(
|
await service.clearLivePresence(
|
||||||
callerUid: callerUid,
|
callerUid: callerUid,
|
||||||
groupRideId: rideId,
|
groupRideId: rideId,
|
||||||
);
|
);
|
||||||
} else if (state.isBroadcasting && state.lastPosition != null) {
|
} else {
|
||||||
// Force immediate publish on opt-in
|
_persistedOptedInRideIds.add(rideId);
|
||||||
await _publishPosition(state.lastPosition!, force: true);
|
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) {
|
void _revokeBroadcasting(String reason) {
|
||||||
|
_persistedOptedInRideIds.remove(rideId);
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
isEligible: false,
|
isEligible: false,
|
||||||
isBroadcasting: false,
|
isBroadcasting: false,
|
||||||
|
|
@ -481,7 +522,7 @@ class LiveBroadcastController extends StateNotifier<LiveBroadcastState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provider for LiveBroadcastController family parameterized by group ride ID.
|
/// Provider for LiveBroadcastController family parameterized by group ride ID.
|
||||||
final liveBroadcastControllerProvider = StateNotifierProvider.autoDispose
|
final liveBroadcastControllerProvider = StateNotifierProvider
|
||||||
.family<LiveBroadcastController, LiveBroadcastState, String>(
|
.family<LiveBroadcastController, LiveBroadcastState, String>(
|
||||||
(ref, rideId) {
|
(ref, rideId) {
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
|
|
|
||||||
|
|
@ -84,14 +84,50 @@ class _RecordingGroupRideService extends Fake implements GroupRideService {
|
||||||
Future<void> respondToJoinRequest({
|
Future<void> respondToJoinRequest({
|
||||||
required String callerUid,
|
required String callerUid,
|
||||||
required String groupRideId,
|
required String groupRideId,
|
||||||
required String requesterUid,
|
|
||||||
required bool approve,
|
required bool approve,
|
||||||
|
required String requesterUid,
|
||||||
}) async {
|
}) async {
|
||||||
lastModeratedCallerUid = callerUid;
|
lastModeratedCallerUid = callerUid;
|
||||||
lastModeratedGroupRideId = groupRideId;
|
lastModeratedGroupRideId = groupRideId;
|
||||||
lastModeratedRequesterUid = requesterUid;
|
lastModeratedRequesterUid = requesterUid;
|
||||||
lastModeratedApprove = approve;
|
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() {
|
void main() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue