Replaces the push/pop stack rooted at Record with a StatefulShellRoute
(Map/Rides/Plan/Settings), each tab keeping its own navigator so Trip Detail
and the route planner push within their own branch. AppShell/ShellScaffold
host one shared RideMap instance behind every tab -- full opacity and
interactive on Map, dimmed and non-interactive elsewhere -- so the camera
position and live path survive a tab switch instead of being refetched.
Every tab's AppBar/header is removed per the redesign's no-chrome mandate.
Fixes a bug this surfaced: RideMap rendered a structurally different tree
for empty vs. non-empty points, which crashed once the map became a
long-lived shell background instead of a fresh per-screen widget. Unified
the background-map tree shape so it no longer remounts mid-recording.
Verified on Medium_Phone_API_35: all four tabs, live recording surviving
tab switches with the camera preserved, and the real HOME-key lifecycle
tile-drop from V3-04 still firing correctly in the shell context.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
Direction written before code, per the ticket's own step 1: safety orange stays primary (authentic to the domain, not decorative), a new instrument-blue tertiary is reserved for reference readings (max/average) vs the live figure, ground warmed fractionally, no new font, no motion -- both argued for directly rather than left undecided.
Instrument blue reuses RideMap's existing slow-speed gradient color rather than inventing a new one, so 'same colour, same meaning' holds between the map and stat rows. contrastRatio() implements WCAG 2.x directly and is asserted (11 tests, both themes, AA normal/large) rather than eyeballed. Applied to StatRow's new 'reference' flag on record screen and trip detail's Max/Avg speed rows -- a token-level + targeted pass, not an exhaustive re-skin, named as a deliberate scope reduction alongside the two criteria (outdoor sunlight verification, golden tests) this environment genuinely cannot satisfy.
Three pure modules: tile_math.dart (tilesForBounds/tilesAlongRoute, hard cap enforced by throwing), tile_cache.dart (FileTileCache with LRU eviction before any write that would exceed the cap), tile_downloader.dart (sequential, rate-limited, cooperatively cancellable, one bad tile doesn't abort the rest). CachedTileProvider wires the cache into flutter_map via a custom ImageProvider and now backs RideMap and RoutePlannerScreen's tile layers, so ordinary viewing write-throughs into the same capped cache.
RoutePlannerScreen gained a route-corridor download action (no rectangle-selection UI -- the ticket names the corridor as strictly better and V3-07 already exists to hang it off of), with a count/size confirmation before any request and a cancellable progress dialog. Fixed a real bug before shipping: a StatefulBuilder-based progress dialog would have started a new overlapping download subscription on every single progress tick; moved to a dedicated StatefulWidget that subscribes once in initState.
Marked partially done: aeroplane-mode verification on a real device is the ticket's own acceptance criterion and needs a phone this environment doesn't have.
Wake lock via a WakelockController seam (mirrors LocationSource), gated strictly to actual recording and released on stop/discard/navigate-away/dispose. High-contrast ripprMountedTheme() applied only to the record screen. BigStat gained a scale param for the headline figure rather than a blanket TextTheme.apply, which crashes on Material 3's default theme.
Feeds RideMap from watchPointsForTrip/watchSegmentsForTrip via autoDispose.family providers. RecordingEngine stays map-free (enforced by architecture_test.dart). RideMap gained lifecycle-aware tile teardown and follow-mode, both shared with the trip-detail map.
flutter_map with the same OSM tiles and no-API-key reasoning that chose osmdroid.
All four load-bearing behaviours ported and, unlike the native app's map, tested:
one polyline per segment so a pause is a real gap, render-only decimation, the
zoom clamp at OSM's max tile zoom with a short-ride fallback, and a real user
agent. Speed colouring is bucketed per run rather than per-vertex, since neither
osmdroid nor flutter_map makes per-vertex paint reasonable.
Export via share_plus, which also handles the iPad popover anchor a naive port
forgets. It passes the raw stored points, never the map's decimated path.
164 tests passing, analyze clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Theme, go_router shell, record screen, trips list with selection, trip detail
with stats and charts, rename/delete/merge. 15 widget tests -- the first this
project has ever had, closing the gap docs/v3/BACKLOG.md names as v2's largest.
The theme sets bodyColor and displayColor explicitly rather than relying on a
wrapping widget, so the black-on-black regression cannot recur by someone
removing a container, and a widget test now asserts the headline names a colour
distinct from the ground.
Those tests immediately found a real layout bug: with a ride active the record
screen grows to six stat rows and overflows a short screen. Compose clipped this
silently, so it may be latent in the native app; Flutter reports it. Fixed by
making the screen scrollable while still centring when there is room.
Two harness lessons. pumpAndSettle never settles against a repeating timer, so
the elapsed clock now runs only while a ride is active -- better behaviour
anyway, since an idle screen has no clock to advance. And flutter_test asserts
no Timer is pending after disposal, which Drift's stream-query keep-alive cache
trips; tests now tear the tree down and pump past that window.
160 tests passing, analyze clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>