FB-01: ambient GPS position drives the shared background map while idle

Adds ambientPositionProvider (never calls LocationSource.stop(), only the
idempotent start()) and wires it into ShellScaffold whenever no trip is
active, so the persistent background map centers and follows the device's
live GPS position at street-level zoom instead of sitting at (0,0)/zoom 2
until a recording starts. RideMap gains an ambientPosition param that the
existing chase-camera/pan-cancel/location-marker logic falls back to
whenever there are no recorded points, with recorded points always taking
priority. Also brings in the docs/feedback ticket set (FB-01..FB-05, README,
FEEDBACK.md) that this worktree's branch point predated.

Adds 10 tests (374 -> 384): RideMap-level ambient centering/chase/pan-cancel/
marker coverage in test/ride_map_test.dart, plus shell-wiring and
provider-level (never-calls-stop, mapEnabledProvider-off) coverage in
test/widget_test.dart. flutter analyze remains clean.
This commit is contained in:
2026-08-24 15:58:45 -05:00
parent 04127d7237
commit 756e3a9f53
12 changed files with 2234 additions and 15 deletions

View File

@@ -57,6 +57,30 @@ final locationSourceProvider = Provider<LocationSource>((ref) {
return source;
});
/// The device's current position when nothing is being recorded — drives the shared
/// background map's "look like Google Maps while idle" behavior. Deliberately NOT
/// gated through `recordingEngineProvider`/`RecordingEngine.start()` — this must work
/// whether or not a ride is ever recorded. Never calls `LocationSource.stop()`: the
/// underlying `locationSourceProvider` instance is shared with the recording engine,
/// and `stop()` is not reference-counted (see FB-01's ticket for why).
final ambientPositionProvider = StreamProvider.autoDispose<LocationFix?>((ref) async* {
if (!ref.watch(mapEnabledProvider)) {
yield null;
return;
}
final source = ref.watch(locationSourceProvider);
try {
await source.start(); // idempotent; safe even if a recording already started it
} on LocationException {
yield null; // permission denied / service disabled — ambient mode is best-effort
return;
}
yield* source.fixes.map<LocationFix?>((fix) => fix);
// No `stop()` call, ever, on dispose — see the doc comment above. Only this
// provider's own subscription to the broadcast `fixes` stream ends; the shared
// platform subscription is left exactly as it was.
});
/// Loaded once at startup; null until then so nothing blocks the first frame.
final configProvider = StateProvider<Config?>((ref) => null);