V3-01: activity type per ride
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>
This commit is contained in:
@@ -45,6 +45,11 @@ class Trips extends Table {
|
||||
|
||||
TextColumn get state => textEnum<domain.TripState>()();
|
||||
|
||||
/// Defaults to motorcycle so existing rows survive the migration that adds this
|
||||
/// column unchanged — see [MigrationStrategy.onUpgrade] below.
|
||||
TextColumn get activity =>
|
||||
textEnum<domain.Activity>().withDefault(const Constant('motorcycle'))();
|
||||
|
||||
RealColumn get distanceM => real().withDefault(const Constant(0))();
|
||||
IntColumn get movingMillis => integer().withDefault(const Constant(0))();
|
||||
RealColumn get maxSpeedKmh => real().withDefault(const Constant(0))();
|
||||
@@ -101,11 +106,18 @@ class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
int get schemaVersion => 2;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onCreate: (m) => m.createAll(),
|
||||
// The port's first real migration. The destructive fallback the native app removed
|
||||
// in v2 stays removed here too — see the file doc comment above.
|
||||
onUpgrade: (m, from, to) async {
|
||||
if (from < 2) {
|
||||
await m.addColumn(trips, trips.activity);
|
||||
}
|
||||
},
|
||||
beforeOpen: (details) async {
|
||||
// Non-negotiable: without this the CASCADE relationships above do nothing.
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
@@ -165,6 +177,7 @@ class AppDatabase extends _$AppDatabase {
|
||||
endedAt: Value(trip.endedAt),
|
||||
name: Value(trip.name),
|
||||
state: trip.state,
|
||||
activity: Value(trip.activity),
|
||||
distanceM: Value(trip.distanceM),
|
||||
movingMillis: Value(trip.movingMillis),
|
||||
maxSpeedKmh: Value(trip.maxSpeedKmh),
|
||||
@@ -173,6 +186,23 @@ class AppDatabase extends _$AppDatabase {
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> setTripActivity(int id, domain.Activity activity) => (update(
|
||||
trips,
|
||||
)..where((t) => t.id.equals(id))).write(
|
||||
TripsCompanion(activity: Value(activity)),
|
||||
);
|
||||
|
||||
/// The most recently created trip, regardless of state — used to default a new ride's
|
||||
/// activity to whatever was last used, so there is no picker in front of Start.
|
||||
Future<domain.Trip?> mostRecentTrip() async {
|
||||
final row =
|
||||
await (select(trips)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.id)])
|
||||
..limit(1))
|
||||
.getSingleOrNull();
|
||||
return row == null ? null : _toTrip(row);
|
||||
}
|
||||
|
||||
Future<void> renameTrip(int id, String? name) => (update(
|
||||
trips,
|
||||
)..where((t) => t.id.equals(id))).write(TripsCompanion(name: Value(name)));
|
||||
@@ -420,6 +450,7 @@ domain.Trip _toTrip(TripRow r) => domain.Trip(
|
||||
endedAt: r.endedAt,
|
||||
name: r.name,
|
||||
state: r.state,
|
||||
activity: r.activity,
|
||||
distanceM: r.distanceM,
|
||||
movingMillis: r.movingMillis,
|
||||
maxSpeedKmh: r.maxSpeedKmh,
|
||||
|
||||
Reference in New Issue
Block a user