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.
10 KiB
FB-02 — Hide the idle Speed panel until recording starts
Depends on — · Size S · Status Not started
Goal
The Record screen's idle (not-recording) state currently shows a live "SPEED" readout plus status text before the rider has pressed Start at all. It should show nothing but the (live, per FB-01) map and the START control until a ride actually begins.
Context
Direct user feedback (docs/FEEDBACK.md, "Map Page" section):
Remove the "Speed" widget from the map screen when "start" hasn't been pressed yet. It blocks the entire screen, and you can't see the map at all. Speed and other widgets should only appear when recording starts.
Today's idle branch — lib/src/ui/record/record_screen.dart, inside build()'s
Stack (~lines 196-223):
child: Stack(
children: [
if (ui.isIdle)
Center(
child: GlassPanel(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Current speed, not max: a max figure only moves when you beat
// it, which reads as a frozen screen while riding steadily.
// This is the 2.0.1 fix and it must not regress.
BigStat(
key: const Key('speed'),
label: 'SPEED',
value: speedValue,
unit: speedUnit,
scale: mountedMode ? mountedTextScale : 1.0,
),
const SizedBox(height: 16),
// A resting state, not a zeroed ride -- otherwise the screen
// looks like a recording that is going nowhere.
const StatRow(label: 'Status', value: 'Ready'),
const StatRow(label: 'Last ride', value: 'See Rides'),
],
),
),
)
else
Positioned.fill(
bottom: _controlBarHeight(mountedMode),
child: HudEditOverlay(
metricBuilder: (context, metric) =>
_HudMetricValue(metric: metric, ui: ui, units: units),
),
),
...
ui.isIdle is trip == null (RecordUiState.isIdle, ~line 42). The GlassPanel sizes
to its content (mainAxisSize: MainAxisSize.min) so it isn't literally full-screen, but
it's centered, opaque-ish (GlassPanel is a near-opaque translucent fill per its own
doc comment elsewhere in this codebase), and shows a real live speed figure
(_liveSpeed, updated every GPS tick via liveTelemetryProvider — see initState
~lines 86-88) before any ride exists — which is exactly what the feedback is describing
as "blocking the screen" and confusing to see before pressing Start.
Design
Remove the entire if (ui.isIdle) ... else ... branching for HUD content — always
render the HudEditOverlay branch, and have the idle state simply show nothing there
(the HUD overlay area is empty when there's no trip, since there's nothing to edit/no
metrics to place — confirm HudEditOverlay degrades gracefully with an idle
RecordUiState, or gate it explicitly: only mount HudEditOverlay when !ui.isIdle,
and render an empty SizedBox.shrink() (or nothing at all) for the idle case, so the
full-bleed live map (per FB-01) is the entire idle screen behind the START control bar.
Concretely:
child: Stack(
children: [
if (!ui.isIdle)
Positioned.fill(
bottom: _controlBarHeight(mountedMode),
child: HudEditOverlay(
metricBuilder: (context, metric) =>
_HudMetricValue(metric: metric, ui: ui, units: units),
),
),
if (ui.isPaused)
...
(i.e. drop the idle Center(child: GlassPanel(...)) block entirely — no replacement
content, not even a "Ready" label. The now-live background map, per FB-01, is the idle
screen's entire content above the control bar.)
speedValue/speedUnit (formatSpeedParts(_liveSpeed, unit: units), ~line 176) become
unused once the idle panel is removed — check whether _HudMetricValue's own
HudMetric.speed case (formatSpeedParts(ui.currentSpeedKmh, unit: units).$1 at
~line 310) already computes this independently (it does — ui.currentSpeedKmh is
_liveSpeed), so the final (speedValue, speedUnit) = ... line in build() should
simply be deleted rather than left as dead code. Run flutter analyze to confirm no
other lingering unused-variable warnings from this removal.
BigStat/StatRow imports in record_screen.dart (from ../components/stats.dart)
may become unused if nothing else in this file still uses them — check before removing
the import; confirmDialog (also from stats.dart) is used elsewhere in this file for
the discard confirmation, so the import itself likely stays, just drop BigStat/
StatRow specifically if nothing else references them.
Implementation
- Remove the idle
Center(child: GlassPanel(...))block fromrecord_screen.dart'sbuild(). - Gate
HudEditOverlayon!ui.isIdle(it was previously theelsebranch of the idle check — now it's the only branch, wrapped in its ownif). - Delete the now-unused
final (speedValue, speedUnit) = formatSpeedParts(...)line. - Run
flutter analyzeand remove any import that's now unused as a result.
Acceptance criteria
- Idle state (no active trip) shows no Speed/Status/"Last ride" content at all — just the live background map and the control bar with only "START RECORDING" visible (unchanged from today).
- The moment a trip starts recording, the full HUD (
HudEditOverlaywith the default-visible metrics: Speed, Avg Speed, Distance, Elapsed Time) appears exactly as it does today — this ticket only changes the idle state, not recording/paused. flutter analyzeclean (no unused-variable/import warnings from the removal).flutter testgreen, test count only goes up (net effect after the test migrations below should still be positive or flat, never negative).
Tests
Several existing tests assert against the idle Speed panel specifically and must be
migrated — not deleted — to seed an active recording and assert against the HUD's own
Speed widget instead, since the underlying regressions they guard (black-on-black
text, mounted-mode contrast/scale) are real risks that still apply once the same figure
lives inside _HudMetricValue instead of the idle BigStat. The HUD's per-metric
widgets are keyed ValueKey(entry.key) where entry.key is the HudMetric
(lib/src/ui/components/hud_edit_overlay.dart ~line 63) — use
find.byKey(const ValueKey(HudMetric.speed)) to locate the speed widget in a recording
state, then descend into it the same way the old tests descended into Key('speed').
In test/widget_test.dart:
'idle shows Ready and only START'(~line 126): remove theexpect(find.text('Ready'), findsOneWidget);assertion (the "Ready"/"Status" text no longer exists at all); keep the START/PAUSE/STOP key assertions and thefind.text('Distance')findsNothing check — both remain valid (this test's title may want updating to'idle shows only START').'the headline is live speed, not max speed'(~line 138): change to seed a recording trip first (await repo.startTrip(1000); await pumpLive(tester, host(const RecordScreen(), map: false));— see the identical pattern already used at ~line 205-206 in'tapping PAUSE and then STOP...'), then assertfind.text('SPEED')findsOneWidget /find.text('MAX SPEED')findsNothing exactly as before (Max Speed is not one of the four default-visible metrics —HudWidgetLayout.defaultFor'svisible: index < columnswithcolumns = 4andHudMetric.valuesorderedspeed, avgSpeed, distance, elapsedTime, maxSpeed, ...— so this assertion still holds true in the recording state).'text is legible against the dark ground'(~line 235): seed a recording trip the same way, then replacefind.descendant(of: find.byKey(const Key('speed')), matching: find.text('0'))withfind.descendant(of: find.byKey(const ValueKey(HudMetric.speed)), matching: find.text('0')). Same downstream assertions (colour != ripprBackground,colour != Colors.black) unchanged.'the mounted theme is high-contrast and text scales up (V3-05)'(~line 380) and'the mounted-mode speed digit is legible against GlassPanel's own translucent surface...'(~line 400): both currently pumpRecordScreen()idle withmountedMode: trueand inspectKey('speed'). Migrate both to seed a recording trip first, then useValueKey(HudMetric.speed)the same way. Confirm_HudMetricValue's valueTextactually honorsmountedMode's scale/theme the same way the old idleBigStatdid (check_HudMetricValue.build()— if it currently has no mounted-mode awareness at all, that's a pre-existing gap this migration would newly expose, not something to paper over: if the mounted-mode font-size/contrast assertions fail against the HUD widget, that's a real, separate bug worth calling out plainly in this ticket's Outcome section rather than weakening the assertion to make it pass.'HUD widgets render over the map, not replacing it (UI-05)'(~line 219) already seeds a recording trip and assertsfind.text('SPEED')/find.text('DISTANCE')— no change needed, but worth a quick sanity check that it still passes unmodified.
Risks
- The mounted-mode migration above may surface that
_HudMetricValuenever actually had mounted-mode-aware styling (it was only ever exercised via the idleBigStatbefore now) — if so, this ticket's Outcome section must say so explicitly rather than silently deleting or weakening the assertion. Fixing that gap (if real) is reasonable to fold into this ticket since it's directly caused by this change, but keep the fix minimal (reuse whatever scale/theme mechanismBigStatalready used) rather than redesigning_HudMetricValue.
Out of scope
The HUD grid/drag-resize/text-auto-fit rework (FB-03) — this ticket only removes idle
content, it does not touch how the recording-state HUD widgets are laid out or sized.
The live-map-while-idle behavior itself (FB-01) — this ticket assumes it lands
separately; if FB-01 hasn't merged yet, the idle screen will just show today's
(0,0)/zoom-2 map, which is still a strict improvement over a blocking Speed panel.