Make exercise tile and settings sheet text-free

Mode chip shows the mode glyph, paused row a pause glyph + 11.10 date
+ cross. Target change reads x13 / = instead of 13 reps / same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-11-11 01:27:59 +01:00
parent 1f8f602564
commit 661b293bc2
10 changed files with 67 additions and 64 deletions

View File

@ -200,23 +200,10 @@ DateTime? parsePauseDate(Object? raw) {
return parsed;
}
const _months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
/// `11 Oct`: how a pause's end date is shown.
String formatShortDate(DateTime d) => '${d.day} ${_months[d.month - 1]}';
/// `11.10`: how a pause's end date is shown (textgate: numerals, no month
/// word; was `11 Oct`).
String formatShortDate(DateTime d) =>
'${d.day}.${d.month.toString().padLeft(2, '0')}';
/// `3:00`: how a rest length is shown.
String formatRest(int secs) =>

View File

@ -125,15 +125,19 @@ ProgressionTarget targetAfterFailure(ProgressionInputs s) {
}
}
/// Short label for moving from [from] to [to]: `25 kg`, `13 reps`,
/// `25 kg × 6`, or `same` when the rule has nowhere left to go.
/// What [describeTargetChange] returns when the rule has nowhere to go.
const kNoTargetChange = '=';
/// Short label for moving from [from] to [to]: `25 kg`, `×13` (reps),
/// `25 kg × 6`, or [kNoTargetChange]. textgate: numerals and symbols only
/// (was `13 reps` / `same`).
String describeTargetChange(ProgressionInputs from, ProgressionTarget to) {
final weightChanged = to.weight != from.weight;
final repsChanged = to.reps != from.reps;
if (weightChanged && repsChanged) return '${_kg(to.weight)} kg × ${to.reps}';
if (weightChanged) return '${_kg(to.weight)} kg';
if (repsChanged) return '${to.reps} reps';
return 'same';
if (repsChanged) return '×${to.reps}';
return kNoTargetChange;
}
/// `25` for 25.0, `27.5` for 27.5 — no trailing `.0` noise in tight rows.

View File

@ -18,7 +18,7 @@ class _DeloadRow extends StatelessWidget {
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final change = describeTargetChange(state, targetAfterFailure(state));
final canDeload = change != 'same';
final canDeload = change != kNoTargetChange;
return SizedBox(
height: 40,
child: Row(

View File

@ -207,16 +207,17 @@ class _PauseRow extends StatelessWidget {
),
]
: [
// textgate: was "Paused — back on 11 Oct".
Icon(
Icons.pause_circle_outline,
size: 18,
color: colorScheme.error,
semanticLabel: 'Paused',
),
const SizedBox(width: 8),
Text(
'Paused — back on ${formatShortDate(until)}',
style: style,
),
Icon(Icons.arrow_forward, size: 16, color: colorScheme.error),
const SizedBox(width: 4),
Text(formatShortDate(until), style: style),
const Spacer(),
TextButton(
onPressed: onResume,

View File

@ -20,10 +20,12 @@ class _ModeChip extends StatelessWidget {
static const double width = 64;
String get _text => switch (state.mode) {
ProgressionMode.weight => 'kg',
ProgressionMode.reps => 'reps',
ProgressionMode.doubleProgression => '${state.repsLow}→${state.repsHigh}',
// textgate: was 'kg' / 'reps'. The same mode glyphs as the settings
// sheet's segmented control; double progression keeps its rep range.
IconData? get _modeIcon => switch (state.mode) {
ProgressionMode.weight => Icons.fitness_center,
ProgressionMode.reps => Icons.repeat,
ProgressionMode.doubleProgression => null,
};
@override
@ -52,12 +54,17 @@ class _ModeChip extends StatelessWidget {
Icon(Icons.tune, size: 13, color: color),
const SizedBox(width: 3),
Flexible(
child: Text(
_text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: color, fontSize: AppTextSize.caption),
),
child: _modeIcon == null
? Text(
'${state.repsLow}→${state.repsHigh}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: color,
fontSize: AppTextSize.caption,
),
)
: Icon(_modeIcon, size: 14, color: color),
),
],
),
@ -204,16 +211,24 @@ class _PausedRow extends StatelessWidget {
height: ExerciseTile.setRowHeight,
child: Row(
children: [
Icon(Icons.pause_circle_outline, size: 20, color: color),
const SizedBox(width: 8),
Expanded(
child: Text(
'Paused — back on ${formatShortDate(until)} · counts as failed',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: color, fontSize: AppTextSize.label),
),
// textgate: was "Paused — back on 11 Oct · counts as failed".
// Now: pause glyph, the return date, and the failed-set cross.
Icon(
Icons.pause_circle_outline,
size: 20,
color: color,
semanticLabel: 'Paused — counts as failed',
),
const SizedBox(width: 8),
Icon(Icons.arrow_forward, size: 16, color: color),
const SizedBox(width: 4),
Text(
formatShortDate(until),
key: const Key('paused_until'),
style: TextStyle(color: color, fontSize: AppTextSize.label),
),
const SizedBox(width: 8),
Icon(Icons.close, size: 16, color: color),
],
),
);

View File

@ -62,9 +62,9 @@ void main() {
});
test('formatShortDate', () {
expect(formatShortDate(DateTime(2026, 10, 11)), '11 Oct');
expect(formatShortDate(DateTime(2026, 1, 1)), '1 Jan');
expect(formatShortDate(DateTime(2026, 12, 31)), '31 Dec');
expect(formatShortDate(DateTime(2026, 10, 11)), '11.10');
expect(formatShortDate(DateTime(2026, 1, 1)), '1.01');
expect(formatShortDate(DateTime(2026, 12, 31)), '31.12');
});
});
}

View File

@ -153,16 +153,13 @@ void main() {
});
test('reps only', () {
expect(
describeTargetChange(from, const ProgressionTarget(20, 9)),
'9 reps',
);
expect(describeTargetChange(from, const ProgressionTarget(20, 9)), '×9');
});
test('no change', () {
expect(
describeTargetChange(from, const ProgressionTarget(20, 8)),
'same',
kNoTargetChange,
);
});

View File

@ -18,6 +18,7 @@ import '../fake_audio_platform.dart';
import '../fake_break_service.dart';
import '../fake_secure_storage.dart';
import '_workout_screen_test_fixtures.dart';
import '../icon_finders.dart';
// Real plan names, so each has a progression-state row to pause.
const _lunge = Exercise(name: 'Dumbbell Lunge', sets: 2, reps: 5, weight: 10);
@ -63,7 +64,7 @@ void main() {
testWidgets('the paused exercise shows no sets and says so', (tester) async {
await pumpPausedSitup(tester);
expect(find.byType(RepCircle), findsNWidgets(_lunge.sets));
expect(find.textContaining('Paused — back on'), findsOneWidget);
expect(iconLabeled('Paused — counts as failed'), findsOneWidget);
});
testWidgets('Finish unlocks without it, and it is recorded as failed', (

View File

@ -155,7 +155,7 @@ void main() {
edits.last.pausedUntil,
pauseEnd(DateTime.now(), kDefaultPauseDays + 1),
);
expect(find.textContaining('Paused — back on'), findsOneWidget);
expect(iconLabeled('Paused'), findsOneWidget);
await tester.tap(iconLabeled('Resume'));
await tester.pump();

View File

@ -134,17 +134,17 @@ void main() {
await tester.pumpWidget(
_wrap(_tile(state: _state.copyWith(mode: ProgressionMode.reps))),
);
expect(find.text('0/3 → 6 reps'), findsOneWidget);
expect(find.text('0/2 → 4 reps'), findsOneWidget);
expect(find.text('0/3 → ×6'), findsOneWidget);
expect(find.text('0/2 → ×4'), findsOneWidget);
});
testWidgets('mode chip names the mode', (tester) async {
testWidgets('mode chip shows the mode glyph', (tester) async {
await tester.pumpWidget(_wrap(_tile()));
expect(find.text('kg'), findsOneWidget);
expect(find.byIcon(Icons.fitness_center), findsOneWidget);
await tester.pumpWidget(
_wrap(_tile(state: _state.copyWith(mode: ProgressionMode.reps))),
);
expect(find.text('reps'), findsOneWidget);
expect(find.byIcon(Icons.repeat), findsOneWidget);
await tester.pumpWidget(
_wrap(
_tile(
@ -184,10 +184,8 @@ void main() {
);
expect(find.byType(RepCircle), findsNothing);
expect(find.bySemanticsLabel('Squat warmup'), findsNothing);
expect(
find.textContaining('back on ${formatShortDate(until)}'),
findsOneWidget,
);
expect(find.text(formatShortDate(until)), findsOneWidget);
expect(iconLabeled('Paused — counts as failed'), findsOneWidget);
expect(tester.getSize(find.byType(ExerciseTile)), size);
expect(_cardColor(tester), idle);
});