diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ecb278c2..44104610 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -140,6 +140,10 @@ "@rir": { "description": "Shorthand for Repetitions In Reserve" }, + "rirNotUsed": "RiR not used", + "@rirNotUsed": { + "description": "Label used in RiR slider when the RiR value is not used/saved for the current setting or log" + }, "weightUnit": "Weight unit", "@weightUnit": {}, "repetitionUnit": "Repetition unit", @@ -188,6 +192,14 @@ "@gymMode": { "description": "Label when starting the gym mode" }, + "pause": "Pause", + "@pause": { + "description": "Noun, not an imperative! Label used for the pause when using the gym mode" + }, + "jumpTo": "Jump to", + "@jumpTo": { + "description": "Imperative. Label used in popup allowing the user to jump to a specific exercise while in the gym mode" + }, "todaysWorkout": "Your workout today", "@todaysWorkout": {}, "logHelpEntries": "If on a single day there is more than one entry with the same number of repetitions, but different weights, only the entry with the higher weight is shown in the diagram.", diff --git a/lib/models/workouts/setting.dart b/lib/models/workouts/setting.dart index 6bc79408..e9df77e1 100644 --- a/lib/models/workouts/setting.dart +++ b/lib/models/workouts/setting.dart @@ -106,11 +106,11 @@ class Setting { repetitionUnitId = repetitionUnit.id; } - void setRir(String rir) { - if (POSSIBLE_RIR_VALUES.contains(rir)) { - this.rir = rir; + void setRir(String newRir) { + if (POSSIBLE_RIR_VALUES.contains(newRir)) { + this.rir = newRir; } else { - throw Exception('RiR value not allowed'); + throw Exception('RiR value not allowed: $newRir'); } } diff --git a/lib/widgets/workouts/forms.dart b/lib/widgets/workouts/forms.dart index 2afd5d65..d03e098e 100644 --- a/lib/widgets/workouts/forms.dart +++ b/lib/widgets/workouts/forms.dart @@ -703,8 +703,23 @@ class WeightInputWidget extends StatelessWidget { class RiRInputWidget extends StatefulWidget { final dynamic _setting; late String dropdownValue; + late double _currentSetSliderValue; + + final SLIDER_START = -0.5; + RiRInputWidget(this._setting) { dropdownValue = _setting.rir != null ? _setting.rir : Setting.DEFAULT_RIR; + + // Read string RiR into a double + if (_setting.rir != null) { + if (_setting.rir == '') { + _currentSetSliderValue = SLIDER_START; + } else { + _currentSetSliderValue = double.parse(_setting.rir); + } + } else { + _currentSetSliderValue = SLIDER_START; + } } @override @@ -712,25 +727,49 @@ class RiRInputWidget extends StatefulWidget { } class _RiRInputWidgetState extends State { + /// Returns the string used in the slider + String getSliderLabel(double value) { + if (value < 0) { + return AppLocalizations.of(context).rirNotUsed; + } + return '${value.toString()} ${AppLocalizations.of(context).rir}'; + } + + String mapDoubleToAllowedRir(double value) { + if (value < 0) { + return ''; + } else { + // The representation is different (3.0 -> 3) we are on an int, round + if (value.toInt() < value) { + return value.toString(); + } else { + return value.toInt().toString(); + } + } + } + @override Widget build(BuildContext context) { - return DropdownButtonFormField( - decoration: InputDecoration(labelText: AppLocalizations.of(context).rir), - value: widget.dropdownValue, - onSaved: (String? newValue) { - widget._setting.setRir(newValue!); - }, - onChanged: (String? newValue) { - setState(() { - widget.dropdownValue = newValue!; - }); - }, - items: Setting.POSSIBLE_RIR_VALUES.map>((String value) { - return DropdownMenuItem( - value: value, - child: Text(value), - ); - }).toList(), + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Text(AppLocalizations.of(context).rir), + Expanded( + child: Slider( + value: widget._currentSetSliderValue, + min: widget.SLIDER_START, + max: (Setting.POSSIBLE_RIR_VALUES.length - 2) / 2, + divisions: Setting.POSSIBLE_RIR_VALUES.length - 1, + label: getSliderLabel(widget._currentSetSliderValue), + onChanged: (double value) { + widget._setting.setRir(mapDoubleToAllowedRir(value)); + setState(() { + widget._currentSetSliderValue = value; + }); + }, + ), + ), + ], ); } } diff --git a/lib/widgets/workouts/gym_mode.dart b/lib/widgets/workouts/gym_mode.dart index 21840fef..28c5a2e4 100644 --- a/lib/widgets/workouts/gym_mode.dart +++ b/lib/widgets/workouts/gym_mode.dart @@ -245,6 +245,7 @@ class LogPage extends StatefulWidget { _log.exercise = _exercise; _log.weightUnit = _setting.weightUnitObj; _log.repetitionUnit = _setting.repetitionUnitObj; + _log.rir = _setting.rir; } @override @@ -256,7 +257,6 @@ class _LogPageState extends State { String rirValue = Setting.DEFAULT_RIR; final _repsController = TextEditingController(); final _weightController = TextEditingController(); - final _rirController = TextEditingController(); var _detailed = false; @override @@ -270,10 +270,6 @@ class _LogPageState extends State { if (widget._setting.weight != null) { _weightController.text = widget._setting.weight.toString(); } - - if (widget._setting.rir != null) { - _rirController.text = widget._setting.rir!; - } } Widget getRepsWidget() { @@ -373,6 +369,124 @@ class _LogPageState extends State { ); } + Widget getForm() { + return Form( + key: _form, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + AppLocalizations.of(context).newEntry, + style: Theme.of(context).textTheme.headline6, + textAlign: TextAlign.center, + ), + if (!_detailed) + Row( + children: [ + Flexible(child: getRepsWidget()), + SizedBox(width: 8), + Flexible(child: getWeightWidget()), + ], + ), + if (_detailed) + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible(child: getRepsWidget()), + SizedBox(width: 8), + Flexible(child: RepetitionUnitInputWidget(widget._log)), + ], + ), + if (_detailed) + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Flexible(child: getWeightWidget()), + SizedBox(width: 8), + Flexible(child: WeightUnitInputWidget(widget._log)) + ], + ), + if (_detailed) RiRInputWidget(widget._log), + SwitchListTile( + title: Text(AppLocalizations.of(context).setUnitsAndRir), + value: _detailed, + onChanged: (value) { + setState(() { + _detailed = !_detailed; + }); + }, + ), + ElevatedButton( + child: Text(AppLocalizations.of(context).save), + onPressed: () async { + // Validate and save the current values to the weightEntry + final isValid = _form.currentState!.validate(); + if (!isValid) { + return; + } + _form.currentState!.save(); + + // Save the entry on the server + try { + await Provider.of(context, listen: false).addLog(widget._log); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: Duration(seconds: 2), // default is 4 + content: Text( + AppLocalizations.of(context).successfullySaved, + textAlign: TextAlign.center, + ), + ), + ); + widget._controller.nextPage( + duration: DEFAULT_ANIMATION_DURATION, + curve: DEFAULT_ANIMATION_CURVE, + ); + } on WgerHttpException catch (error) { + showHttpExceptionErrorDialog(error, context); + } catch (error) { + showErrorDialog(error, context); + } + }, + ), + ], + ), + ); + } + + Widget getPastLogs() { + return ListView( + children: [ + Text( + AppLocalizations.of(context).labelWorkoutLogs, + style: Theme.of(context).textTheme.headline6, + textAlign: TextAlign.center, + ), + ...widget._workoutPlan.filterLogsByExercise(widget._exercise, unique: true).map((log) { + return ListTile( + title: Text(log.singleLogRepText.replaceAll('\n', '')), + subtitle: + Text(DateFormat.yMd(Localizations.localeOf(context).languageCode).format(log.date)), + trailing: Icon(Icons.copy), + onTap: () { + setState(() { + // Text field + _repsController.text = log.reps.toString(); + _weightController.text = log.weight.toString(); + + // Drop downs + widget._log.rir = log.rir; + widget._log.repetitionUnit = log.repetitionUnitObj; + widget._log.weightUnit = log.weightUnitObj; + }); + }, + contentPadding: EdgeInsets.symmetric(horizontal: 40), + ); + }).toList(), + ], + ); + } + @override Widget build(BuildContext context) { return Column( @@ -392,119 +506,14 @@ class _LogPageState extends State { SizedBox(height: 10), Expanded( child: (widget._workoutPlan.filterLogsByExercise(widget._exercise).length > 0) - ? ListView( - children: [ - Text( - AppLocalizations.of(context).labelWorkoutLogs, - style: Theme.of(context).textTheme.headline6, - textAlign: TextAlign.center, - ), - ...widget._workoutPlan - .filterLogsByExercise(widget._exercise, unique: true) - .map((log) { - return ListTile( - title: Text(log.singleLogRepText.replaceAll('\n', '')), - subtitle: Text( - DateFormat.yMd(Localizations.localeOf(context).languageCode) - .format(log.date)), - trailing: Icon(Icons.copy), - onTap: () { - setState(() { - // Text field - _repsController.text = log.reps.toString(); - _weightController.text = log.weight.toString(); - - // Drop downs - widget._log.rir = log.rir; - widget._log.repetitionUnit = log.repetitionUnitObj; - widget._log.weightUnit = log.weightUnitObj; - }); - }, - contentPadding: EdgeInsets.symmetric(horizontal: 40), - ); - }).toList(), - ], - ) + ? getPastLogs() : Container()), SizedBox(height: 15), Padding( padding: const EdgeInsets.symmetric(horizontal: 15), - child: Form( - key: _form, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (!_detailed) - Row( - children: [ - Flexible(child: getRepsWidget()), - SizedBox(width: 8), - Flexible(child: getWeightWidget()), - ], - ), - if (_detailed) - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Flexible(child: getRepsWidget()), - SizedBox(width: 8), - Flexible(child: RepetitionUnitInputWidget(widget._log)), - ], - ), - if (_detailed) - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Flexible(child: getWeightWidget()), - SizedBox(width: 8), - Flexible(child: WeightUnitInputWidget(widget._log)) - ], - ), - if (_detailed) RiRInputWidget(widget._log), - IconButton( - icon: Icon(_detailed ? Icons.unfold_less : Icons.unfold_more), - onPressed: () { - setState(() { - _detailed = !_detailed; - }); - }, - ), - ElevatedButton( - child: Text(AppLocalizations.of(context).save), - onPressed: () async { - // Validate and save the current values to the weightEntry - final isValid = _form.currentState!.validate(); - if (!isValid) { - return; - } - _form.currentState!.save(); - - // Save the entry on the server - try { - await Provider.of(context, listen: false) - .addLog(widget._log); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: Duration(seconds: 2), // default is 4 - content: Text( - AppLocalizations.of(context).successfullySaved, - textAlign: TextAlign.center, - ), - ), - ); - widget._controller.nextPage( - duration: Duration(milliseconds: 200), - curve: Curves.bounceIn, - ); - } on WgerHttpException catch (error) { - showHttpExceptionErrorDialog(error, context); - } catch (error) { - showErrorDialog(error, context); - } - }, - ), - ], - ), + child: Card( + color: wgerBackground, + child: getForm(), ), ), NavigationFooter(widget._controller, widget._ratioCompleted), @@ -822,7 +831,7 @@ class _TimerWidgetState extends State { return Column( children: [ NavigationHeader( - '', + AppLocalizations.of(context).pause, widget._controller, exercisePages: widget._exercisePages, ), @@ -900,7 +909,10 @@ class NavigationHeader extends StatelessWidget { Widget getDialog(BuildContext context) { return AlertDialog( - title: Text('Jump to', textAlign: TextAlign.center), + title: Text( + AppLocalizations.of(context).jumpTo, + textAlign: TextAlign.center, + ), contentPadding: EdgeInsets.zero, content: SingleChildScrollView( child: Column( diff --git a/test/gym_mode_screen_test.dart b/test/gym_mode_screen_test.dart index 19b15a00..a4bca19a 100644 --- a/test/gym_mode_screen_test.dart +++ b/test/gym_mode_screen_test.dart @@ -105,7 +105,7 @@ void main() { expect(find.text('test exercise 1'), findsOneWidget); expect(find.byType(LogPage), findsOneWidget); expect(find.byType(Form), findsOneWidget); - expect(find.byType(ListTile), findsNWidgets(2)); + expect(find.byType(ListTile), findsNWidgets(3), reason: 'Two logs and the switch tile'); expect(find.text('10 × 10 kg (1.5 RiR)'), findsOneWidget); expect(find.text('12 × 10 kg (2 RiR)'), findsOneWidget); expect(find.byIcon(Icons.close), findsOneWidget); @@ -114,20 +114,18 @@ void main() { expect(find.byIcon(Icons.chevron_right), findsOneWidget); // Form shows only weight and reps - expect(find.byIcon(Icons.unfold_more), findsOneWidget); + expect(find.byType(SwitchListTile), findsOneWidget); expect(find.byType(TextFormField), findsNWidgets(2)); expect(find.byType(RepetitionUnitInputWidget), findsNothing); expect(find.byType(WeightUnitInputWidget), findsNothing); expect(find.byType(RiRInputWidget), findsNothing); // Form shows unit and rir after tapping the toggle button - await tester.tap(find.byIcon(Icons.unfold_more)); + await tester.tap(find.byType(SwitchListTile)); await tester.pump(); expect(find.byType(RepetitionUnitInputWidget), findsOneWidget); expect(find.byType(WeightUnitInputWidget), findsOneWidget); expect(find.byType(RiRInputWidget), findsOneWidget); - expect(find.byIcon(Icons.unfold_less), findsOneWidget); - await tester.drag(find.byType(LogPage), Offset(-500.0, 0.0)); await tester.pumpAndSettle();