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..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; @@ -1195,7 +1196,17 @@ 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 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; + } } /// Write the log through — to the draft, so minimising cannot lose it, and @@ -1226,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(() { @@ -1317,6 +1329,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..324b8373 100644 --- a/test/ui2_activity_test.dart +++ b/test/ui2_activity_test.dart @@ -1309,6 +1309,43 @@ 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, + }, + ]); + + List banked = []; + await tester.pumpWidget(_frame( + 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-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); + }); + testWidgets('a collapsed group builds none of its rows', (tester) async { tester.view.physicalSize = const Size(390 * 3, 1400 * 3); tester.view.devicePixelRatio = 3; @@ -1462,6 +1499,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);