Refresh the Flutter snapshot: UI-01 through UI-09, plus a fresh installable APK

Full UI redesign pass complete: persistent tab shell with an always-visible
background map, Modern Professional Dark theme, monochrome dark map tiles,
offline skeleton map, a shared GlassPanel/FloatingPill component kit,
customizable HUD telemetry widgets, and the Map HUD / Plan & Route Planning /
Rides History screen rebuilds. 374 tests passing, up from 316.

The APK is a fresh release build (debug-signed, no release signing config
exists yet).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
This commit is contained in:
2026-08-24 14:41:35 -05:00
parent 4c634354bd
commit 587f75eab4
42 changed files with 4401 additions and 89 deletions

View File

@@ -0,0 +1,154 @@
# UI-09 — Monochrome dark map tiles
**Depends on** nothing · **Size** S · **Status** Done
## Goal
Replace the tan/cream default OpenStreetMap raster style with a monochrome dark map, so
the map actually looks like it belongs in a dark-mode HUD instead of a light basemap
punched through a dark overlay.
## Context
`RideMap`, the Plan/Route Planning canvas, and every Stitch mockup all assume a dark or
near-monochrome map underneath the HUD. Today's `TileLayer` (`ride_map.dart`) points at
`tile.openstreetmap.org`, OSM's standard light/tan default style — the actual on-device
result is nothing like the mockups, regardless of how dark the chrome on top of it is.
This affects every screen that shows a map (Map HUD, Plan, Route Planning, and UI-01's
dimmed background on every other tab), so it's worth settling once, early, rather than
each screen ticket discovering the same mismatch independently.
## Design
Two real options, not one obvious answer:
**Option A — switch tile provider to CARTO's "Dark Matter" basemap.**
`https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png` (subdomains a-d, retina
`{r}` variant available, max native zoom 20). Purpose-built dark, desaturated/monochrome
cartography — closest to the mockups with the least engineering. Free for reasonable
usage same as OSM's own tiles, but it's a **different third party with its own usage
policy and attribution requirement** ("© OpenStreetMap contributors © CARTO", not just
OSM's own attribution) — a real dependency to add, not a style tweak.
**Option B — keep OSM tiles, apply a color filter client-side.** Wrap `TileLayer` in a
`ColorFiltered` widget using a `ColorFilter.matrix` that desaturates and inverts/darkens
the tan basemap into a monochrome dark look. No new third party, no new attribution, and
V3-11's offline tile cache keeps working against the exact tiles it already knows about
— but the result is a filtered light map, not tiles actually designed for dark
presentation, and will read as slightly "off" next to CARTO's purpose-built version
(road/building contrast tuned for light backgrounds doesn't always invert cleanly).
**Recommendation: Option A.** The mockups' whole aesthetic depends on the map itself
being dark, not just tinted dark, and CARTO's dark tiles are a well-established, free,
no-API-key option already widely used in the Flutter/Leaflet ecosystem for exactly this.
The added attribution line and a second host to trust are a small, one-time cost.
**V3-11 interaction:** the offline tile cache (`FileTileCache`/`CachedTileProvider`) is
already keyed by `TileKey(z, x, y)` with no provider-specific data baked in — but that
means a cache built against OSM tiles and a cache built against CARTO tiles are
**indistinguishable to the cache** despite being visually different. Switching providers
without a plan here would silently serve stale, wrong-looking cached OSM tiles once
CARTO's URL is live. This needs a cache-busting or provider-tagging fix as part of this
ticket, not a separate one — see Implementation.
## Implementation
1. Add a `provider`-qualified cache key (or a cache-version bump that invalidates
existing V3-11 cache contents wholesale) so switching tile sources can't silently
mix cached tiles from two visually different sources.
2. Update `RideMap`'s `TileLayer` (and the Route Planner's separate `TileLayer` instance
— see `route_planner_screen.dart`) to CARTO's dark tile URL template, correct
`subdomains`, and `maxNativeZoom: 20`.
3. Update the attribution requirement — check whether `flutter_map`'s
`RichAttributionWidget`/`AttributionWidget` is in use anywhere already, or add one;
OSM-only attribution is no longer sufficient once CARTO tiles are in the mix.
4. Update `tileUserAgent`/any OSM-specific comments in `ride_map.dart` that assumed OSM's
tile servers specifically (the "respect OSM's usage policy" reasoning still applies in
spirit to CARTO's own policy, but the specific server being addressed changes).
5. Re-verify V3-11's tile math/download flow still works end-to-end against the new URL
template (tile enumeration and caching are provider-agnostic already; only the fetch
URL construction needs updating).
## Acceptance criteria
- [ ] The map renders in a dark, desaturated/monochrome style matching the Stitch
mockups, not OSM's default tan basemap
- [ ] Correct attribution for both OpenStreetMap and CARTO is shown wherever the map is
shown
- [ ] Switching tile providers does not silently serve stale, visually-mismatched
cached tiles from before the switch (V3-11's cache is invalidated or
provider-tagged)
- [ ] Both `RideMap` and the Route Planner's independent `TileLayer` are updated
consistently — one dark style everywhere, not just on the screens that happened to
get touched first
## Tests
- Existing `ride_map`/route-planner widget tests updated for the new tile URL and still
passing
- Unit: cache-key/versioning change is exercised the same way V3-11's eviction tests
are — a cache built under the old scheme does not get served for the new provider
## Risks
- **A second third-party tile host is a real dependency, not a free color swap** — its
own usage policy, its own risk of being blocked if misused, and its own attribution
obligation. Treat it with the same care V3-11 already applies to OSM (rate-limit
respecting, no bulk prefetch, real user agent).
- If CARTO's free tier ever proves insufficient or is discontinued, Option B (a color
filter over OSM tiles) is the fallback with no new third-party dependency — worth
keeping in mind as a documented Plan B, not re-deriving from scratch later.
## Out of scope
A fully custom/self-hosted vector tile style — much larger scope, and not needed to hit
"looks like the mockups" today.
## Outcome
Took the recommended Option A. `ride_map.dart` now defines `tileUrlTemplate`
(`https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png`), `tileSubdomains`
(`a`-`d`), and `tileMaxNativeZoom` (20, kept separate from the app's own `maxTileZoom`
clamp of 19 — raising the app's overall zoom ceiling to match CARTO's native maximum was
deliberately left alone rather than folded into this ticket, since it would mean
re-verifying the fit/follow-zoom logic V3-04/V3-05 already tuned against 19). Both
`RideMap` and `route_planner_screen.dart`'s independent `TileLayer` now import and use
these same three constants plus `retinaMode: true`, so there is exactly one dark style
and one set of tile-request parameters across the app, not two independently-drifting
copies.
**Cache versioning (the ticket's named risk):** `tileCacheProvider` in
`app/providers.dart` now points `FileTileCache` at `tiles/carto_dark_v1` instead of the
old bare `tiles` directory. `TileKey(z, x, y)` still carries no provider identity, so
the directory segment is the actual version tag — any tiles cached under the old OSM-tan
scheme are simply orphaned in a directory the app no longer reads from, rather than
being silently served under the new dark UI. Added a unit test
(`test/tile_cache_test.dart`, "a tile cached under one provider directory is not served
from another") proving this isolation holds at the `FileTileCache` level, the same way
V3-11's own eviction tests exercise cache behavior directly rather than through the UI.
**Attribution:** added a shared `TileAttribution` widget (`ride_map.dart`) wrapping
flutter_map's `RichAttributionWidget` with `TextSourceAttribution`s for both
"OpenStreetMap contributors" (CARTO's dark style is still built from OSM's underlying
data) and "CARTO" itself — OSM-only credit, which is what the app carried before, stopped
being sufficient the moment a second tile host entered the mix. Composed into both
`RideMap` and the route planner's `FlutterMap`, so it's one component discharging the
obligation everywhere a map renders, not a copy-pasted attribution block per screen. Did
not wire up `onTap` license-page links — that would need the `url_launcher` package, a
new dependency this ticket has no other reason to add; the obligation is to visibly
credit both sources, not to make the credit tappable.
**Tests:** `flutter analyze` clean. `flutter test` green at 316 tests (315 + the new
cache-isolation test) — no existing test hardcoded the old OSM URL string, so nothing
else needed updating.
**Android emulator verification** (`Medium_Phone_API_35`): confirmed CARTO's dark,
desaturated basemap renders on-device (a genuinely dark map, not a tan basemap under a
dark overlay) — a clear visual match for the Stitch mockups' aesthetic, on the Map tab at
full opacity and dimmed correctly on Rides/Plan/Settings via UI-01's existing scrim. The
attribution icon is visibly present in the bottom-left corner on every tab (same shared
map instance). Its tap-to-expand interaction could not be confirmed via `adb input tap`
in this session — taps at its on-screen coordinates didn't visibly toggle the popup, and
a genuine Android mock-location watermark icon (left over from this session's earlier
`adb emu geo fix` calls, and confirmed present at the same screen position across
unrelated tabs, which a page-level widget couldn't be) sits immediately next to it,
making the exact tap target ambiguous to hit blindly. This is a dev-tooling
verification gap, not a known defect — the widget itself renders without error and
matches flutter_map's standard, widely-shipped attribution pattern (the same
small-icon-that-expands convention Google Maps and Mapbox both use). Re-verify the
popup's tap behavior with a real touchscreen or Flutter Inspector if it becomes load-
bearing later (e.g., if legal review specifically requires confirming the expand
interaction, not just the icon's presence).