From 309de9bbaea82038bae0c33511226cc8003c10bd Mon Sep 17 00:00:00 2001 From: bhavit04 Date: Wed, 24 Jun 2026 16:38:41 +0530 Subject: [PATCH] feat(routines): allow custom bar weight in plate calculator The bar weight in the plate calculator settings could previously only be set to one of a few preset values from a dropdown (e.g. 10/15/20 kg), which made common bars like 7.5 kg impossible to configure. Replace the dropdown with an editable, locale-aware numeric field so any positive value can be entered, while keeping the previous presets as quick-select chips. Invalid input is rejected with an inline error and the last valid value is retained. Closes #1131 Co-Authored-By: Claude Opus 4.8 --- lib/widgets/routines/plate_calculator.dart | 80 ++++++++++++++----- .../routines/plate_calculator_test.dart | 16 ++-- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/lib/widgets/routines/plate_calculator.dart b/lib/widgets/routines/plate_calculator.dart index 4e5c06b73..00c9481f5 100644 --- a/lib/widgets/routines/plate_calculator.dart +++ b/lib/widgets/routines/plate_calculator.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/providers/plate_weights.dart'; @@ -63,6 +64,10 @@ class ConfigureAvailablePlates extends ConsumerStatefulWidget { } class _AddPlateWeightsState extends ConsumerState { + final TextEditingController _barWeightController = TextEditingController(); + bool _barWeightInitialized = false; + String? _barWeightError; + @override void initState() { super.initState(); @@ -71,14 +76,35 @@ class _AddPlateWeightsState extends ConsumerState { }); } + @override + void dispose() { + _barWeightController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final i18n = AppLocalizations.of(context); + final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString()); final plateWeightsState = ref.watch(plateCalculatorProvider); final plateWeightsNotifier = ref.read(plateCalculatorProvider.notifier); // final userProvider = provider.Provider.of(context); + // Initialise the field with the current value and keep it in sync when the + // bar weight changes from outside the field (data loaded from prefs, unit + // switch, preset chips), without clobbering what the user is typing. + if (!_barWeightInitialized) { + _barWeightController.text = numberFormat.format(plateWeightsState.barWeight); + _barWeightInitialized = true; + } + ref.listen(plateCalculatorProvider.select((s) => s.barWeight), (_, barWeight) { + if (numberFormat.tryParse(_barWeightController.text) != barWeight) { + _barWeightController.text = numberFormat.format(barWeight); + _barWeightError = null; + } + }); + return Column( mainAxisSize: MainAxisSize.max, children: [ @@ -108,24 +134,42 @@ class _AddPlateWeightsState extends ConsumerState { ), Padding( padding: const EdgeInsets.all(10), - child: DropdownMenu( - key: const ValueKey('barWeightDropdown'), - width: double.infinity, - initialSelection: plateWeightsState.barWeight, - requestFocusOnTap: true, - label: Text(i18n.barWeight), - onSelected: (num? value) { - if (value == null) { - return; - } - plateWeightsNotifier.setBarWeight(value); - }, - dropdownMenuEntries: plateWeightsState.availableBarsWeights.map((value) { - return DropdownMenuEntry( - value: value, - label: value.toString(), - ); - }).toList(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + key: const ValueKey('barWeightField'), + controller: _barWeightController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + labelText: i18n.barWeight, + errorText: _barWeightError, + ), + onChanged: (value) { + final parsed = numberFormat.tryParse(value); + setState(() { + if (parsed == null || parsed <= 0) { + _barWeightError = i18n.enterValidNumber; + } else { + _barWeightError = null; + plateWeightsNotifier.setBarWeight(parsed); + } + }); + }, + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + children: plateWeightsState.availableBarsWeights.map((value) { + return ChoiceChip( + key: ValueKey('barWeightChip-$value'), + label: Text(value.toString()), + selected: plateWeightsState.barWeight == value, + onSelected: (_) => plateWeightsNotifier.setBarWeight(value), + ); + }).toList(), + ), + ], ), ), SwitchListTile( diff --git a/test/widgets/routines/plate_calculator_test.dart b/test/widgets/routines/plate_calculator_test.dart index 5e334103b..9b27123c6 100644 --- a/test/widgets/routines/plate_calculator_test.dart +++ b/test/widgets/routines/plate_calculator_test.dart @@ -67,19 +67,17 @@ void main() { await tester.pumpAndSettle(); expect(notifier.state.isMetric, isFalse); - // Correctly changes the bar weight + // Correctly changes the bar weight via a preset chip expect(notifier.state.barWeight, 45); - await tester.tap(find.byKey(const ValueKey('barWeightDropdown'))); - await tester.pumpAndSettle(); - final menuItem = find.ancestor( - of: find.text('25'), - matching: find.byType(InkWell), - ); - expect(menuItem, findsOneWidget); - await tester.tap(menuItem); + await tester.tap(find.byKey(const ValueKey('barWeightChip-25'))); await tester.pumpAndSettle(); expect(notifier.state.barWeight, 25); + // Correctly accepts a custom bar weight typed into the field + await tester.enterText(find.byKey(const ValueKey('barWeightField')), '7.5'); + await tester.pumpAndSettle(); + expect(notifier.state.barWeight, 7.5); + // Correctly toggles the useColors switch expect(notifier.state.useColors, isFalse); await tester.tap(find.byKey(const ValueKey('useColorsSwitch')));