mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Make RiR form element a slider
This allows the user to select the correct value faster
This commit is contained in:
@@ -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.",
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<RiRInputWidget> {
|
||||
/// 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<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
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;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<LogPage> {
|
||||
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<LogPage> {
|
||||
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<LogPage> {
|
||||
);
|
||||
}
|
||||
|
||||
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<WorkoutPlansProvider>(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<LogPage> {
|
||||
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<WorkoutPlansProvider>(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<TimerWidget> {
|
||||
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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user