From 330ad382a4706cc6fd9e901920d5d92e355f06d9 Mon Sep 17 00:00:00 2001 From: Yehonatan Avrahimi <48332126+yontank@users.noreply.github.com> Date: Wed, 28 Jan 2026 23:40:17 +0200 Subject: [PATCH 1/2] BUGFIX: unfocus while typing in gym_mode form --- lib/widgets/routines/gym_mode/log_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index 49865e7e..a9b131ef 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -251,7 +251,7 @@ class LogsRepsWidget extends ConsumerWidget { child: TextFormField( decoration: InputDecoration(labelText: i18n.repetitions), enabled: true, - key: ValueKey('reps-field-$repText'), + key: const ValueKey('reps-field'), initialValue: repText, keyboardType: textInputTypeDecimal, onChanged: (value) { @@ -332,7 +332,7 @@ class LogsWeightWidget extends ConsumerWidget { // Text field Expanded( child: TextFormField( - key: ValueKey('weight-field-$weightText'), + key: const ValueKey('weight-field'), decoration: InputDecoration(labelText: i18n.weight), initialValue: weightText, keyboardType: textInputTypeDecimal, From f06a15ebac1e5b4beb5c7d30ebbc8f01bfd3f2aa Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Thu, 29 Jan 2026 17:43:31 +0100 Subject: [PATCH 2/2] Convert the reps and weight log widgets back to stateful. This solves some problems with the keyboard focus changing or the quick-add buttons for reps and weight. Also, add some tests for these. --- lib/widgets/routines/gym_mode/log_page.dart | 116 +++++++++++++---- .../routines/gym_mode/log_page_test.dart | 120 ++++++++++++++++++ 2 files changed, 211 insertions(+), 25 deletions(-) diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index a9b131ef..fd40effe 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -211,26 +211,61 @@ class LogsPlatesWidget extends ConsumerWidget { } } -class LogsRepsWidget extends ConsumerWidget { - final _logger = Logger('LogsRepsWidget'); - +class LogsRepsWidget extends ConsumerStatefulWidget { final num valueChange; - LogsRepsWidget({ + const LogsRepsWidget({ super.key, num? valueChange, }) : valueChange = valueChange ?? 1; @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _LogsRepsWidgetState(); +} + +class _LogsRepsWidgetState extends ConsumerState { + final _logger = Logger('LogsRepsWidget'); + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString()); final i18n = AppLocalizations.of(context); final logNotifier = ref.read(gymLogProvider.notifier); final log = ref.watch(gymLogProvider); - final currentReps = log?.repetitions; - final repText = currentReps != null ? numberFormat.format(currentReps) : ''; + final currentInput = numberFormat.tryParse(_controller.text); + + // Sync from provider to controller if needed + if (currentReps != null) { + // Update if values differ, but allow invalid input while typing (unless empty/initial) + if (currentInput != currentReps && (currentInput != null || _controller.text.isEmpty)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _controller.text = numberFormat.format(currentReps); + } + }); + } + } else if (_controller.text.isNotEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _controller.clear(); + } + }); + } return Row( children: [ @@ -239,7 +274,7 @@ class LogsRepsWidget extends ConsumerWidget { icon: const Icon(Icons.remove, color: Colors.black), onPressed: () { final base = currentReps ?? 0; - final newValue = base - valueChange; + final newValue = base - widget.valueChange; if (newValue >= 0 && log != null) { logNotifier.setRepetitions(newValue); } @@ -251,8 +286,7 @@ class LogsRepsWidget extends ConsumerWidget { child: TextFormField( decoration: InputDecoration(labelText: i18n.repetitions), enabled: true, - key: const ValueKey('reps-field'), - initialValue: repText, + controller: _controller, keyboardType: textInputTypeDecimal, onChanged: (value) { try { @@ -282,7 +316,7 @@ class LogsRepsWidget extends ConsumerWidget { icon: const Icon(Icons.add, color: Colors.black), onPressed: () { final base = currentReps ?? 0; - final newValue = base + valueChange; + final newValue = base + widget.valueChange; if (newValue >= 0 && log != null) { logNotifier.setRepetitions(newValue); } @@ -293,27 +327,62 @@ class LogsRepsWidget extends ConsumerWidget { } } -class LogsWeightWidget extends ConsumerWidget { - final _logger = Logger('LogsWeightWidget'); - +class LogsWeightWidget extends ConsumerStatefulWidget { final num valueChange; - LogsWeightWidget({ + const LogsWeightWidget({ super.key, num? valueChange, }) : valueChange = valueChange ?? 1.25; @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _LogsWeightWidgetState(); +} + +class _LogsWeightWidgetState extends ConsumerState { + final _logger = Logger('LogsWeightWidget'); + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString()); final i18n = AppLocalizations.of(context); final plateProvider = ref.read(plateCalculatorProvider.notifier); final logProvider = ref.read(gymLogProvider.notifier); final log = ref.watch(gymLogProvider); - final currentWeight = log?.weight; - final weightText = currentWeight != null ? numberFormat.format(currentWeight) : ''; + final currentInput = numberFormat.tryParse(_controller.text); + + // Sync from provider to controller if needed + if (currentWeight != null) { + // Update if values differ, but allow invalid input while typing (unless empty/initial) + if (currentInput != currentWeight && (currentInput != null || _controller.text.isEmpty)) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _controller.text = numberFormat.format(currentWeight); + } + }); + } + } else if (_controller.text.isNotEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + _controller.clear(); + } + }); + } return Row( children: [ @@ -322,7 +391,7 @@ class LogsWeightWidget extends ConsumerWidget { icon: const Icon(Icons.remove, color: Colors.black), onPressed: () { final base = currentWeight ?? 0; - final newValue = base - valueChange; + final newValue = base - widget.valueChange; if (newValue >= 0 && log != null) { logProvider.setWeight(newValue); } @@ -332,9 +401,8 @@ class LogsWeightWidget extends ConsumerWidget { // Text field Expanded( child: TextFormField( - key: const ValueKey('weight-field'), + controller: _controller, decoration: InputDecoration(labelText: i18n.weight), - initialValue: weightText, keyboardType: textInputTypeDecimal, onChanged: (value) { try { @@ -365,7 +433,7 @@ class LogsWeightWidget extends ConsumerWidget { icon: const Icon(Icons.add, color: Colors.black), onPressed: () { final base = currentWeight ?? 0; - final newValue = base + valueChange; + final newValue = base + widget.valueChange; if (log != null) { logProvider.setWeight(newValue); } @@ -408,9 +476,7 @@ class LogsPastLogsWidget extends ConsumerWidget { logProvider.setLog(pastLog); ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context).dataCopied), - ), + SnackBar(content: Text(AppLocalizations.of(context).dataCopied)), ); }, contentPadding: const EdgeInsets.symmetric(horizontal: 40), diff --git a/test/widgets/routines/gym_mode/log_page_test.dart b/test/widgets/routines/gym_mode/log_page_test.dart index b9ba0f8d..cda24260 100644 --- a/test/widgets/routines/gym_mode/log_page_test.dart +++ b/test/widgets/routines/gym_mode/log_page_test.dart @@ -237,5 +237,125 @@ void main() { expect(capturedLog!.routineId, equals(notifier.state.routine.id)); expect(capturedLog!.iteration, equals(notifier.state.iteration)); }); + + testWidgets('LogsRepsWidget quick buttons update values', (tester) async { + // Arrange + final notifier = container.read(gymStateProvider.notifier); + final routine = testdata.getTestRoutine(); + routine.dayDataGym[0].slots[0].setConfigs[0].repetitions = 0; + notifier.state = notifier.state.copyWith( + dayId: routine.days.first.id, + routine: routine, + iteration: 1, + ); + notifier.calculatePages(); + notifier.setCurrentPage(2); + notifier.state = notifier.state.copyWith(currentPage: 2); + final mockRoutines = MockRoutinesProvider(); + await pumpLogPage(tester, routinesProvider: mockRoutines); + + // Act + final repsWidgetFinder = find.byKey(const ValueKey('logs-reps-widget')); + expect(repsWidgetFinder, findsOneWidget); + final addBtn = find.descendant( + of: repsWidgetFinder, + matching: find.byIcon(Icons.add), + ); + final removeBtn = find.descendant( + of: repsWidgetFinder, + matching: find.byIcon(Icons.remove), + ); + + // Assert + // Increment 0 -> 1 + await tester.tap(addBtn); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pumpAndSettle(); + expect( + find.descendant(of: repsWidgetFinder, matching: find.text('1')), + findsOneWidget, + ); + + // Increment 1 -> 2 + await tester.tap(addBtn); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pumpAndSettle(); + expect( + find.descendant(of: repsWidgetFinder, matching: find.text('2')), + findsOneWidget, + ); + + // Decrement 2 -> 1 + await tester.tap(removeBtn); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pumpAndSettle(); + expect( + find.descendant(of: repsWidgetFinder, matching: find.text('1')), + findsOneWidget, + ); + }); + + testWidgets('LogsWeightWidget quick buttons update values', (tester) async { + // Arrange + final notifier = container.read(gymStateProvider.notifier); + final routine = testdata.getTestRoutine(); + routine.dayDataGym[0].slots[0].setConfigs[0].weight = 0; + notifier.state = notifier.state.copyWith( + dayId: routine.days.first.id, + routine: routine, + iteration: 1, + ); + notifier.calculatePages(); + notifier.setCurrentPage(2); + notifier.state = notifier.state.copyWith(currentPage: 2); + final mockRoutines = MockRoutinesProvider(); + await pumpLogPage(tester, routinesProvider: mockRoutines); + + // Act + final weightWidgetFinder = find.byKey(const ValueKey('logs-weight-widget')); + expect(weightWidgetFinder, findsOneWidget); + final addBtn = find.descendant( + of: weightWidgetFinder, + matching: find.byIcon(Icons.add), + ); + final removeBtn = find.descendant( + of: weightWidgetFinder, + matching: find.byIcon(Icons.remove), + ); + + // Assert + // Increment 0 -> 1.25 + await tester.tap(addBtn); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pumpAndSettle(); + expect( + find.descendant(of: weightWidgetFinder, matching: find.text('1.25')), + findsOneWidget, + ); + + // Increment 1.25 -> 2.5 + await tester.tap(addBtn); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pumpAndSettle(); + expect( + find.descendant(of: weightWidgetFinder, matching: find.text('2.5')), + findsOneWidget, + ); + + // Decrement 2.5 -> 1.25 + await tester.tap(removeBtn); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pumpAndSettle(); + expect( + find.descendant(of: weightWidgetFinder, matching: find.text('1.25')), + findsOneWidget, + ); + }); }); }