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.
Built together because V3-02's settings screen needed something real for
V3-03's units to control -- executed in reverse of the ticket numbering, but
both tickets are independently complete.
V3-03: UnitSystem (metric/imperial) lives in domain/models.dart alongside
Activity, defaulting from Platform.localeName on first launch. Storage stays
SI everywhere -- conversion happens only in ui/format.dart, at the last
possible moment, which is now documented as the file's central invariant.
Threaded through all three screens plus the speed histogram's bucket labels,
which relabel for display without changing how speedHistogram itself bins.
Export was deliberately left untouched: gpx()/geoJson() take no UnitSystem
parameter at all, a stronger guarantee than validating one would be.
V3-02: SettingsScreen reachable from the record screen. Map render toggle
(nothing previously exposed mapEnabledProvider to the user, despite it
existing since T15 -- there was nothing to "move off trip detail" as drafted),
unit selector, upload endpoint with http(s) validation, a copyable device id,
and a licences page via Flutter's built-in showLicensePage. Skipped
package_info_plus (static version string instead) and a privacy-policy link
(none published yet) as disproportionate to an S-sized ticket.
Neither ticket needed the ConfigNotifier the implementation notes proposed:
unitSystemProvider reuses the exact StateProvider-seeded-from-Config pattern
mapEnabledProvider already established, since Config mutates its own backing
SharedPreferences in place and re-assigning the same instance would never
notify a watcher anyway.
A real locale-dependent flake was caught, not just anticipated: a test
asserting a fresh Config defaults to metric failed, because this machine's own
locale resolves to a region in the imperial set. Fixed by seeding an explicit
value before asserting, and config_test.dart's locale test was written from
the start to only prove the fallback path doesn't throw, not to assert which
value it returns.
221 tests (211 -> 221): 13 format_test, 9 config_test, 9 settings_screen_test,
1 confirming the record screen's settings button is genuinely wired. Analyze
clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds Activity (motorcycle/bicycle/scooter/skateboard/running/walking/other) as
a column on Trip, and threads it through as real behaviour rather than a label:
ActivityProfile (domain/activity_profile.dart) carries a noise floor, accuracy
gate, histogram bucket width, and elevation smoothing window/threshold per
activity, consumed by sanitizeSpeedKmh, isUsableFix, computeSummary, Accumulator
and speedHistogram. The motorcycle profile reproduces the exact constants the
app shipped with before this existed, and a test asserts they never drift apart.
This is the port's first real migration: schemaVersion 1 -> 2,
m.addColumn(trips, trips.activity) with a motorcycle default so every existing
row survives unmodified. Proven with a hand-built v1 SQLite file (raw sqlite3,
not drift_dev's schema tooling) that a real database with real rides upgrades
and keeps every trip, segment and point.
No picker in front of Start: a new trip defaults to whichever activity was most
recently used, derived live from the trips table rather than duplicated into a
Config field. Editable afterwards on trip detail, which recomputes aggregates
under the new profile immediately -- a walking pace that reads as noise under a
motorcycle's floor reads as real movement once the activity is corrected, and
there's a test proving exactly that transition.
The widget-test pass for the activity-picker sheet caught a real overflow bug:
seven options overflowed a Column-based bottom sheet the same way the record
screen once did. Fixed with a scrollable ListView + isScrollControlled, same
shape as that earlier fix.
188 tests (171 -> 188): 2 migration, 5 ActivityProfile, 3 profile-threading
proofs in computeSummary, 5 repository, 2 widget. Analyze clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>