From cc843670ddb282162b57d08beb1ca4fbbcc4d75b Mon Sep 17 00:00:00 2001 From: highdeftant <78290151+highdeftant@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:04:02 -0400 Subject: [PATCH 1/3] feat: add push-ups to strength logging --- lib/ui2/activity/catalogue.dart | 8 +++++++ lib/ui2/activity/live.dart | 3 +++ test/db_strength_schema_test.dart | 16 ++++++++++++- test/ui2_activity_test.dart | 39 +++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/lib/ui2/activity/catalogue.dart b/lib/ui2/activity/catalogue.dart index 59743b9d..a379a199 100644 --- a/lib/ui2/activity/catalogue.dart +++ b/lib/ui2/activity/catalogue.dart @@ -381,6 +381,14 @@ const exerciseLibrary = [ ExerciseDef('leg_press', 'Leg press', {'legs': .75, 'glutes': .25}), ExerciseDef('plank', 'Plank', {'core': 1.0}, step: 0), ExerciseDef('hanging_leg_raise', 'Hanging leg raise', {'core': 1.0}, step: 0), + // Bodyweight movement: repetitions are user-entered; this is not inferred + // from wearable motion and carries no external load estimate. + ExerciseDef( + 'push_up', + 'Push-ups', + {'chest': .6, 'triceps': .25, 'shoulders': .15}, + step: 0, + ), ]; final Map _exercisesByKey = { diff --git a/lib/ui2/activity/live.dart b/lib/ui2/activity/live.dart index 4504fdc9..fafe7f1c 100644 --- a/lib/ui2/activity/live.dart +++ b/lib/ui2/activity/live.dart @@ -1317,6 +1317,9 @@ class _LiveStrengthState extends State { setState(() { if (!plan.contains(picked)) plan.add(picked); index = plan.indexOf(picked); + // A zero increment marks bodyweight-only exercises. Start there unless + // the user's saved history below has a more specific previous choice. + bodyweight = exerciseByKey(picked)?.step == 0; }); _seedFromHistory(); } diff --git a/test/db_strength_schema_test.dart b/test/db_strength_schema_test.dart index b6ec2a77..80d6e4ba 100644 --- a/test/db_strength_schema_test.dart +++ b/test/db_strength_schema_test.dart @@ -111,10 +111,22 @@ void main() { 'rpe': 8, 'at_ts': 1770000600, }, + { + 'exercise_key': 'push_up', + 'set_index': 1, + 'reps': 10, + 'at_ts': 1770000900, + }, + { + 'exercise_key': 'push_up', + 'set_index': 2, + 'reps': 8, + 'at_ts': 1770001200, + }, ]); final sets = await LocalDb.strengthSets('sess-1'); - expect(sets, hasLength(2)); + expect(sets, hasLength(4)); expect(sets[0]['seq'], 0); expect(sets[0]['load_kg'], 80.0); expect(sets[1]['load_kg'], isNull, @@ -137,6 +149,8 @@ void main() { final recent = await LocalDb.recentSetsFor('pull_up'); expect(recent, hasLength(1)); expect(recent.first['reps'], 9); + final pushUpHistory = await LocalDb.recentSetsFor('push_up'); + expect(pushUpHistory.map((s) => s['reps']), unorderedEquals([10, 8])); expect(await LocalDb.recentSetsFor('nothing_here'), isEmpty); }); diff --git a/test/ui2_activity_test.dart b/test/ui2_activity_test.dart index be4898af..dcf70248 100644 --- a/test/ui2_activity_test.dart +++ b/test/ui2_activity_test.dart @@ -1462,6 +1462,45 @@ void main() { expect(tester.takeException(), isNull); }); + testWidgets('push-ups start as bodyweight and repeated sets are banked', + (tester) async { + tester.view.physicalSize = const Size(390 * 3, 2600 * 3); + tester.view.devicePixelRatio = 3; + addTearDown(tester.view.reset); + addTearDown(LiveDraft.clear); + + List banked = const []; + await tester.pumpWidget(_frame( + LiveStrength( + activityByName('weight_training')!, + onSets: (sets) => banked = List.of(sets), + ), + Brightness.light, + 1.0)); + await tester.pumpAndSettle(); + await tester.tap(find.bySemanticsLabel('Next exercise')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Push-ups')); + await tester.pumpAndSettle(); + + expect(find.text('Push-ups'), findsOneWidget); + expect(find.text('Bodyweight — left out of volume'), findsOneWidget); + await tester.tap(find.text('Log set')); + await tester.pump(); + expect(banked, hasLength(1)); + expect(banked.single.exerciseKey, 'push_up'); + expect(banked.single.reps, 8); + expect(banked.single.loadKg, isNull); + + await tester.tap(find.text('Skip rest')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Log set')); + await tester.pump(); + expect(banked, hasLength(2)); + expect(StrengthLog(banked).repCount, 16); + expect(banked.every((set) => set.exerciseKey == 'push_up'), isTrue); + }); + testWidgets('the picker lists every activity and searches by name', (tester) async { tester.view.physicalSize = const Size(390 * 3, 1400 * 3); From ffd4440caacdf546d19f4b1dea2dae6ecf322a72 Mon Sep 17 00:00:00 2001 From: highdeftant <78290151+highdeftant@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:11:41 -0400 Subject: [PATCH 2/3] fix: restore bodyweight mode for push-up drafts --- lib/ui2/activity/live.dart | 7 ++++++- test/ui2_activity_test.dart | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/ui2/activity/live.dart b/lib/ui2/activity/live.dart index fafe7f1c..17079409 100644 --- a/lib/ui2/activity/live.dart +++ b/lib/ui2/activity/live.dart @@ -1195,7 +1195,12 @@ class _LiveStrengthState extends State { )); if (!plan.contains(k)) plan.add(k); } - if (logged.isNotEmpty) index = plan.indexOf(logged.last.exerciseKey); + if (logged.isNotEmpty) { + index = plan.indexOf(logged.last.exerciseKey); + // The draft restores the selected exercise and sets, but not the input + // mode. Restore a bodyweight default before history can override it. + bodyweight = def?.step == 0; + } } /// Write the log through — to the draft, so minimising cannot lose it, and diff --git a/test/ui2_activity_test.dart b/test/ui2_activity_test.dart index dcf70248..493a4806 100644 --- a/test/ui2_activity_test.dart +++ b/test/ui2_activity_test.dart @@ -1309,6 +1309,35 @@ void main() { expect(tester.takeException(), isNull); }); + testWidgets('a restored push-up draft keeps bodyweight mode', + (tester) async { + tester.view.physicalSize = const Size(390 * 3, 2400 * 3); + tester.view.devicePixelRatio = 3; + addTearDown(tester.view.reset); + addTearDown(LiveDraft.clear); + + final a = activityByName('weight_training')!; + LiveDraft.begin(a, weightKg: 72.4); + LiveDraft.current!.put('sets', [ + { + 'k': 'push_up', + 'reps': 12, + 'kg': null, + 'rpe': 7, + 'rest': null, + 'at': DateTime(2026, 9, 24).millisecondsSinceEpoch, + }, + ]); + + await tester.pumpWidget(_frame( + LiveStrength(a), Brightness.light, 1.0)); + await tester.pumpAndSettle(); + + expect(find.text('Bodyweight — left out of volume'), findsOneWidget, + reason: 'a restored zero-increment exercise must not default to 40 kg'); + expect(tester.takeException(), isNull); + }); + testWidgets('a collapsed group builds none of its rows', (tester) async { tester.view.physicalSize = const Size(390 * 3, 1400 * 3); tester.view.devicePixelRatio = 3; From 75961bac599224348851bd5f9945f9d8bbe2dd33 Mon Sep 17 00:00:00 2001 From: highdeftant <78290151+highdeftant@users.noreply.github.com> Date: Thu, 24 Sep 2026 11:15:29 -0400 Subject: [PATCH 3/3] fix: preserve restored strength input state --- lib/ui2/activity/live.dart | 13 ++++++++++--- test/ui2_activity_test.dart | 12 ++++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/ui2/activity/live.dart b/lib/ui2/activity/live.dart index 17079409..84ed757e 100644 --- a/lib/ui2/activity/live.dart +++ b/lib/ui2/activity/live.dart @@ -1146,6 +1146,7 @@ class _LiveStrengthState extends State { bool bodyweight = false; final logged = []; + String? _draftInputKey; static const restTarget = 90; @@ -1197,9 +1198,14 @@ class _LiveStrengthState extends State { } if (logged.isNotEmpty) { index = plan.indexOf(logged.last.exerciseKey); - // The draft restores the selected exercise and sets, but not the input - // mode. Restore a bodyweight default before history can override it. - bodyweight = def?.step == 0; + // The latest restored set is the session's saved input state; preserve + // its bodyweight/load choice and keep history seeding from replacing it. + final last = logged.last; + _draftInputKey = last.exerciseKey; + bodyweight = last.loadKg == null; + kg = last.loadKg ?? kg; + reps = last.reps; + rpe = last.rpe ?? rpe; } } @@ -1231,6 +1237,7 @@ class _LiveStrengthState extends State { /// Open each exercise at what the user did last time. Nothing to go on → /// leave the stepper where it is rather than guessing a load. void _seedFromHistory() { + if (_draftInputKey == key) return; final prev = widget.history[key]?.previous; if (prev == null) return; setState(() { diff --git a/test/ui2_activity_test.dart b/test/ui2_activity_test.dart index 493a4806..324b8373 100644 --- a/test/ui2_activity_test.dart +++ b/test/ui2_activity_test.dart @@ -1329,12 +1329,20 @@ void main() { }, ]); + List banked = []; await tester.pumpWidget(_frame( - LiveStrength(a), Brightness.light, 1.0)); + LiveStrength(a, onSets: (sets) => banked = sets), + Brightness.light, + 1.0)); await tester.pumpAndSettle(); expect(find.text('Bodyweight — left out of volume'), findsOneWidget, - reason: 'a restored zero-increment exercise must not default to 40 kg'); + reason: 'a restored zero-load set must keep bodyweight mode'); + await tester.tap(find.text('Log set')); + await tester.pump(); + expect(banked.last.exerciseKey, 'push_up'); + expect(banked.last.loadKg, isNull, + reason: 'the next set must not inherit the 40 kg default'); expect(tester.takeException(), isNull); });