Files
flutter/lib/widgets/routines/forms/slot.dart
Roland Geider 6bfcb3ebe9 Round the value of the slider
This prevents float values from being sent to the server
2025-01-07 21:02:22 +01:00

576 lines
19 KiB
Dart

/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (C) 2020, 2021 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* wger Workout Manager is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/models/exercises/exercise.dart';
import 'package:wger/models/workouts/day.dart';
import 'package:wger/models/workouts/slot.dart';
import 'package:wger/models/workouts/slot_entry.dart';
import 'package:wger/providers/routines.dart';
import 'package:wger/widgets/exercises/autocompleter.dart';
import 'package:wger/widgets/routines/forms/reps_unit.dart';
import 'package:wger/widgets/routines/forms/rir.dart';
import 'package:wger/widgets/routines/forms/weight_unit.dart';
class ProgressionRulesInfoBox extends StatelessWidget {
final Exercise exercise;
const ProgressionRulesInfoBox(this.exercise, {super.key});
@override
Widget build(BuildContext context) {
final languageCode = Localizations.localeOf(context).languageCode;
final i18n = AppLocalizations.of(context);
return Column(
children: [
ListTile(
title: Text(
exercise.getExercise(languageCode).name,
style: Theme.of(context).textTheme.titleMedium,
),
),
ListTile(
leading: const Icon(Icons.info),
tileColor: Theme.of(context).colorScheme.primaryContainer,
title: Text(
i18n.progressionRules,
style: Theme.of(context).textTheme.bodySmall,
),
),
],
);
}
}
class SlotEntryForm extends StatefulWidget {
final SlotEntry entry;
final bool simpleMode;
const SlotEntryForm(this.entry, {this.simpleMode = true, super.key});
@override
State<SlotEntryForm> createState() => _SlotEntryFormState();
}
class _SlotEntryFormState extends State<SlotEntryForm> {
final iconSize = 18.0;
double setsSliderValue = 1.0;
final weightController = TextEditingController();
final maxWeightController = TextEditingController();
final repsController = TextEditingController();
final maxRepsController = TextEditingController();
final restController = TextEditingController();
final maxRestController = TextEditingController();
final rirController = TextEditingController();
final _form = GlobalKey<FormState>();
var _edit = false;
@override
void initState() {
super.initState();
if (widget.entry.nrOfSetsConfigs.isNotEmpty) {
setsSliderValue = widget.entry.nrOfSetsConfigs.first.value.toDouble();
}
if (widget.entry.weightConfigs.isNotEmpty) {
weightController.text = widget.entry.weightConfigs.first.value.toString();
}
if (widget.entry.maxWeightConfigs.isNotEmpty) {
maxWeightController.text = widget.entry.maxWeightConfigs.first.value.toString();
}
if (widget.entry.repsConfigs.isNotEmpty) {
repsController.text = widget.entry.repsConfigs.first.value.round().toString();
}
if (widget.entry.maxRepsConfigs.isNotEmpty) {
maxRepsController.text = widget.entry.maxRepsConfigs.first.value.round().toString();
}
if (widget.entry.restTimeConfigs.isNotEmpty) {
restController.text = widget.entry.restTimeConfigs.first.value.round().toString();
}
if (widget.entry.maxRestTimeConfigs.isNotEmpty) {
maxRestController.text = widget.entry.maxRestTimeConfigs.first.value.round().toString();
}
if (widget.entry.rirConfigs.isNotEmpty) {
rirController.text = widget.entry.rirConfigs.first.value.round().toString();
}
}
@override
void dispose() {
weightController.dispose();
maxWeightController.dispose();
repsController.dispose();
maxRepsController.dispose();
restController.dispose();
maxRestController.dispose();
rirController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context);
final languageCode = Localizations.localeOf(context).languageCode;
return Form(
key: _form,
child: Column(
children: [
ListTile(
title: Text(
widget.entry.exerciseObj.getExercise(languageCode).name,
style: Theme.of(context).textTheme.titleMedium,
// textAlign: TextAlign.center,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {
setState(() {
_edit = !_edit;
});
},
icon: _edit
? Icon(Icons.edit_off, size: iconSize)
: Icon(Icons.edit, size: iconSize),
),
IconButton(
icon: Icon(Icons.delete, size: iconSize),
onPressed: () {
context.read<RoutinesProvider>().deleteSlotEntry(widget.entry.id!);
},
),
],
),
),
if (_edit)
ExerciseAutocompleter(
onExerciseSelected: (exercise) => {
setState(() {
widget.entry.exercise = exercise;
})
},
),
Row(
children: [
Text('${i18n.sets}: ${setsSliderValue.round()}'),
Expanded(
child: Slider(
value: setsSliderValue,
min: 1,
max: 20,
divisions: 20,
label: setsSliderValue.round().toString(),
onChanged: (double value) {
setState(() {
setsSliderValue = value;
});
},
),
),
],
),
if (!widget.simpleMode)
WeightUnitInputWidget(
widget.entry.weightUnitId,
onChanged: (value) {
widget.entry.weightUnitId = value;
},
),
Row(
spacing: 10,
children: [
Flexible(
child: TextFormField(
controller: weightController,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: i18n.weight),
validator: (value) {
if (value != null && value != '' && double.tryParse(value) == null) {
return i18n.enterValidNumber;
}
return null;
},
),
),
if (!widget.simpleMode)
Flexible(
child: TextFormField(
controller: maxWeightController,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: i18n.max),
validator: (value) {
if (value != null && value != '' && double.tryParse(value) == null) {
return i18n.enterValidNumber;
}
return null;
},
),
),
],
),
if (!widget.simpleMode)
RepetitionUnitInputWidget(
widget.entry.repetitionUnitId,
onChanged: (value) {
widget.entry.repetitionUnitId = value;
},
),
Row(
spacing: 10,
children: [
Flexible(
child: TextFormField(
controller: repsController,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: i18n.repetitions),
validator: (value) {
if (value != null && value != '' && int.tryParse(value) == null) {
return i18n.enterValidNumber;
}
return null;
},
),
),
if (!widget.simpleMode)
Flexible(
child: TextFormField(
controller: maxRepsController,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: i18n.max),
validator: (value) {
if (value != null && value != '' && int.tryParse(value) == null) {
return i18n.enterValidNumber;
}
return null;
},
),
),
],
),
if (!widget.simpleMode)
Row(
spacing: 10,
children: [
Flexible(
child: TextFormField(
controller: restController,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: i18n.restTime),
validator: (value) {
if (value != null && value != '' && int.tryParse(value) == null) {
return i18n.enterValidNumber;
}
return null;
},
),
),
Flexible(
child: TextFormField(
controller: maxRestController,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: i18n.max),
validator: (value) {
if (value != null && value != '' && int.tryParse(value) == null) {
return i18n.enterValidNumber;
}
return null;
},
),
),
],
),
if (!widget.simpleMode)
RiRInputWidget(
rirController.text == '' ? null : num.parse(rirController.text),
onChanged: (value) => rirController.text = value,
),
const SizedBox(height: 5),
OutlinedButton(
key: const Key(SUBMIT_BUTTON_KEY_NAME),
child: Text(i18n.save),
onPressed: () async {
if (!_form.currentState!.validate()) {
return;
}
_form.currentState!.save();
final provider = Provider.of<RoutinesProvider>(context, listen: false);
// Process new, edited or entries to be deleted
provider.handleConfig(
widget.entry,
setsSliderValue == 0 ? '' : setsSliderValue.round().toString(),
ConfigType.sets,
);
provider.handleConfig(
widget.entry,
weightController.text,
ConfigType.weight,
);
provider.handleConfig(
widget.entry,
maxWeightController.text,
ConfigType.maxWeight,
);
provider.handleConfig(
widget.entry,
repsController.text,
ConfigType.reps,
);
provider.handleConfig(
widget.entry,
maxRepsController.text,
ConfigType.maxReps,
);
provider.handleConfig(
widget.entry,
restController.text,
ConfigType.rest,
);
provider.handleConfig(
widget.entry,
maxRestController.text,
ConfigType.maxRest,
);
provider.handleConfig(
widget.entry,
rirController.text,
ConfigType.rir,
);
provider.editSlotEntry(widget.entry);
},
),
const SizedBox(height: 10),
],
),
);
}
}
class SlotDetailWidget extends StatefulWidget {
final Slot slot;
final bool simpleMode;
const SlotDetailWidget(this.slot, {this.simpleMode = true, super.key});
@override
State<SlotDetailWidget> createState() => _SlotDetailWidgetState();
}
class _SlotDetailWidgetState extends State<SlotDetailWidget> {
bool _addSuperset = false;
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context);
final provider = context.read<RoutinesProvider>();
return Column(
children: [
...widget.slot.entries.map((entry) => entry.hasProgressionRules
? ProgressionRulesInfoBox(entry.exerciseObj)
: SlotEntryForm(entry, simpleMode: widget.simpleMode)),
const SizedBox(height: 10),
if (_addSuperset || widget.slot.entries.isEmpty)
ExerciseAutocompleter(
onExerciseSelected: (exercise) async {
final SlotEntry entry = SlotEntry.withData(
slotId: widget.slot.id!,
order: widget.slot.entries.length + 1,
exercise: exercise,
);
_addSuperset = false;
await provider.addSlotEntry(entry);
},
),
if (widget.slot.entries.isNotEmpty)
FilledButton(
onPressed: () {
setState(() {
_addSuperset = !_addSuperset;
});
},
child: Text(i18n.addSuperset),
),
const SizedBox(height: 5),
],
);
}
}
class ReorderableSlotList extends StatefulWidget {
final List<Slot> slots;
final Day day;
const ReorderableSlotList(this.slots, this.day);
@override
_SlotFormWidgetStateNg createState() => _SlotFormWidgetStateNg();
}
class _SlotFormWidgetStateNg extends State<ReorderableSlotList> {
int? selectedSlotId;
bool simpleMode = true;
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context);
final provider = context.read<RoutinesProvider>();
final languageCode = Localizations.localeOf(context).languageCode;
return Column(
children: [
if (!widget.day.isRest)
SwitchListTile(
value: simpleMode,
title: Text(i18n.simpleMode),
subtitle: Text(i18n.simpleModeHelp),
contentPadding: const EdgeInsets.all(4),
onChanged: (value) {
setState(() {
simpleMode = value;
});
},
),
ReorderableListView.builder(
buildDefaultDragHandles: false,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: widget.slots.length,
itemBuilder: (context, index) {
final slot = widget.slots[index];
final isCurrentSlotSelected = slot.id == selectedSlotId;
return Card(
color: slot.entries.isEmpty ? Theme.of(context).colorScheme.inversePrimary : null,
key: ValueKey(slot.id),
child: Column(
children: [
ListTile(
title: slot.isSuperset
? Text(i18n.supersetNr(index + 1))
: Text(i18n.exerciseNr(index + 1)),
tileColor: isCurrentSlotSelected ? Theme.of(context).highlightColor : null,
leading: selectedSlotId == null
? ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
)
: const Icon(Icons.block),
subtitle: slot.entries.isEmpty
? Text(i18n.setHasNoExercises)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...slot.entries
.map((e) => Text(e.exerciseObj.getExercise(languageCode).name)),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {
setState(() {
if (selectedSlotId == slot.id) {
selectedSlotId = null;
} else {
selectedSlotId = slot.id;
}
});
},
icon: isCurrentSlotSelected
? const Icon(Icons.edit_off)
: const Icon(Icons.edit),
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
selectedSlotId = null;
await provider.deleteSlot(slot.id!);
}),
],
),
),
if (isCurrentSlotSelected) SlotDetailWidget(slot, simpleMode: simpleMode),
],
),
);
},
onReorder: (int oldIndex, int newIndex) {
setState(() {
// Update the order of slots in your data source
if (oldIndex < newIndex) {
newIndex -= 1;
}
final item = widget.slots.removeAt(oldIndex);
widget.slots.insert(newIndex, item);
for (int i = 0; i < widget.slots.length; i++) {
widget.slots[i].order = i + 1;
}
provider.editSlots(widget.slots);
});
},
),
if (!widget.day.isRest)
ListTile(
leading: const Icon(Icons.add),
title: Text(
i18n.addExercise,
style: Theme.of(context).textTheme.titleMedium,
),
onTap: () async {
final newSlot = await provider.addSlot(Slot.withData(
day: widget.day.id,
order: widget.slots.length + 1,
));
setState(() {
selectedSlotId = newSlot.id;
});
},
),
],
);
}
}