Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/ui2/activity/catalogue.dart
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,14 @@ const exerciseLibrary = <ExerciseDef>[
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<String, ExerciseDef> _exercisesByKey = {
Expand Down
17 changes: 16 additions & 1 deletion lib/ui2/activity/live.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,7 @@ class _LiveStrengthState extends State<LiveStrength> {
bool bodyweight = false;

final logged = <LoggedSet>[];
String? _draftInputKey;

static const restTarget = 90;

Expand Down Expand Up @@ -1195,7 +1196,17 @@ class _LiveStrengthState extends State<LiveStrength> {
));
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
Expand Down Expand Up @@ -1226,6 +1237,7 @@ class _LiveStrengthState extends State<LiveStrength> {
/// 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(() {
Expand Down Expand Up @@ -1317,6 +1329,9 @@ class _LiveStrengthState extends State<LiveStrength> {
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;
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
_seedFromHistory();
}
Expand Down
16 changes: 15 additions & 1 deletion test/db_strength_schema_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
});

Expand Down
76 changes: 76 additions & 0 deletions test/ui2_activity_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<LoggedSet> 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;
Expand Down Expand Up @@ -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<LoggedSet> 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);
Expand Down