From 8020bd21b6ca30bb6bcdd8ced5ed21475d18fb2f Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sat, 16 Nov 2024 22:11:05 +0100 Subject: [PATCH] More polish and i18n work Also make sure that we don't update the routine while editing, since some of the changes could pull the rug under our feet and felt strange. Now there's a manual refresh button for the resulting routine --- lib/l10n/app_en.arb | 13 +++++ lib/providers/routines.dart | 2 +- lib/widgets/core/settings.dart | 10 +++- lib/widgets/dashboard/widgets.dart | 4 +- lib/widgets/routines/day.dart | 5 +- lib/widgets/routines/forms/day.dart | 72 ++++++++++++++++-------- lib/widgets/routines/forms/routine.dart | 16 ++++-- lib/widgets/routines/forms/slot.dart | 75 +++++++++++++------------ lib/widgets/routines/routine_edit.dart | 53 +++++++++++++++-- 9 files changed, 177 insertions(+), 73 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9b031fd2..bab70e0b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -156,6 +156,10 @@ "@reps": { "description": "Shorthand for repetitions, used when space constraints are tighter" }, + "sets": "Sets", + "@sets": { + "description": "The number of sets to be done for one exercise" + }, "rir": "RiR", "@rir": { "description": "Shorthand for Repetitions In Reserve" @@ -201,6 +205,11 @@ "@workoutSession": { "description": "A (logged) workout session" }, + "restDay": "Rest day", + "isRestDay": "Is rest day", + "isRestDayHelp": "Please consider that all sets are removed from rest days when saved", + "routineDays": "Days in routine", + "resultingRoutine": "Resulting routine", "newDay": "New day", "@newDay": {}, "newSet": "New set", @@ -697,7 +706,11 @@ "images": "Images", "language": "Language", "addExercise": "Add exercise", + "fitInWeek": "Fit in week", + "fitInWeekHelp": "Select if you want to fit the workout days into a week, you can add e.g. three days and mark this checkbox.", "addSuperset": "Add superset", + "setHasProgression": "Set has progression", + "setHasProgressionWarning": "Please note that at the moment it is not possible to edit all settings for a set on the mobile application or configure the progression. ", "setHasNoExercises": "This set has no exercises yet!", "contributeExercise": "Contribute an exercise", "translation": "Translation", diff --git a/lib/providers/routines.dart b/lib/providers/routines.dart index 59e9f8ef..9df751ee 100644 --- a/lib/providers/routines.dart +++ b/lib/providers/routines.dart @@ -419,7 +419,7 @@ class RoutinesProvider with ChangeNotifier { day = Day.fromJson(data); day.slots = []; final routine = findById(day.routineId); - routine.days.insert(0, day); + routine.days.add(day); if (refresh) { fetchAndSetRoutineFull(day.routineId); } diff --git a/lib/widgets/core/settings.dart b/lib/widgets/core/settings.dart index 78c8dfa9..9dde8f8b 100644 --- a/lib/widgets/core/settings.dart +++ b/lib/widgets/core/settings.dart @@ -30,6 +30,7 @@ class SettingsPage extends StatelessWidget { @override Widget build(BuildContext context) { + final i18n = AppLocalizations.of(context); final exerciseProvider = Provider.of(context, listen: false); final nutritionProvider = Provider.of(context, listen: false); @@ -40,7 +41,12 @@ class SettingsPage extends StatelessWidget { body: ListView( children: [ ListTile( - title: Text(AppLocalizations.of(context).settingsExerciseCacheDescription), + title: Text( + i18n.settingsCacheTitle, + style: Theme.of(context).textTheme.headlineSmall, + )), + ListTile( + title: Text(i18n.settingsExerciseCacheDescription), trailing: IconButton( key: const ValueKey('cacheIconExercises'), icon: const Icon(Icons.delete), @@ -49,7 +55,7 @@ class SettingsPage extends StatelessWidget { if (context.mounted) { final snackBar = SnackBar( - content: Text(AppLocalizations.of(context).settingsCacheDeletedSnackbar), + content: Text(i18n.settingsCacheDeletedSnackbar), ); ScaffoldMessenger.of(context).showSnackBar(snackBar); diff --git a/lib/widgets/dashboard/widgets.dart b/lib/widgets/dashboard/widgets.dart index 10c9ffb9..c2abad70 100644 --- a/lib/widgets/dashboard/widgets.dart +++ b/lib/widgets/dashboard/widgets.dart @@ -394,7 +394,9 @@ class _DashboardWorkoutWidgetState extends State { children: [ Expanded( child: Text( - dayData.day == null || dayData.day!.isRest ? 'REST DAY' : dayData.day!.name, + dayData.day == null || dayData.day!.isRest + ? AppLocalizations.of(context).restDay + : dayData.day!.name, style: const TextStyle(fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, ), diff --git a/lib/widgets/routines/day.dart b/lib/widgets/routines/day.dart index 3c74faa1..46d2644d 100644 --- a/lib/widgets/routines/day.dart +++ b/lib/widgets/routines/day.dart @@ -17,6 +17,7 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:wger/models/workouts/day_data.dart'; import 'package:wger/models/workouts/set_config_data.dart'; import 'package:wger/models/workouts/slot_data.dart'; @@ -135,13 +136,15 @@ class DayHeader extends StatelessWidget { @override Widget build(BuildContext context) { + final i18n = AppLocalizations.of(context); + if (_dayData.day == null || _dayData.day!.isRest) { return ListTile( // tileColor: Colors.amber, tileColor: Theme.of(context).focusColor, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), title: Text( - 'REST DAY', + i18n.restDay, style: Theme.of(context).textTheme.headlineSmall, overflow: TextOverflow.ellipsis, ), diff --git a/lib/widgets/routines/forms/day.dart b/lib/widgets/routines/forms/day.dart index db547feb..1dcd3cfc 100644 --- a/lib/widgets/routines/forms/day.dart +++ b/lib/widgets/routines/forms/day.dart @@ -25,8 +25,8 @@ class ReorderableDaysList extends StatefulWidget { context: context, builder: (BuildContext context) { return AlertDialog( - title: const Text('Confirm Delete'), - content: const Text('Are you sure you want to delete this day?'), + title: Text(AppLocalizations.of(context).delete), + content: Text(AppLocalizations.of(context).confirmDelete(day.name)), actions: [ TextButton( onPressed: () { @@ -55,6 +55,9 @@ class ReorderableDaysList extends StatefulWidget { class _ReorderableDaysListState extends State { @override Widget build(BuildContext context) { + final i18n = AppLocalizations.of(context); + final provider = context.read(); + return Column( children: [ ReorderableListView.builder( @@ -72,13 +75,13 @@ class _ReorderableDaysListState extends State { //selected: day.id == widget.selectedDayId, tileColor: isDaySelected ? Theme.of(context).highlightColor : null, key: ValueKey(day), - title: Text(day.isRest ? 'REST DAY!' : day.name), + title: Text(day.isRest ? i18n.restDay : day.name), leading: ReorderableDragStartListener( index: index, child: const Icon(Icons.drag_handle), ), subtitle: Text( - day.isRest ? '' : day.description, + day.description, style: const TextStyle(overflow: TextOverflow.ellipsis), ), trailing: Row( @@ -117,7 +120,7 @@ class _ReorderableDaysListState extends State { } }); - Provider.of(context, listen: false).editDays(widget.days); + provider.editDays(widget.days); }, ), ListTile( @@ -129,11 +132,17 @@ class _ReorderableDaysListState extends State { style: Theme.of(context).textTheme.titleMedium, ), onTap: () async { - final newDay = Day.empty(); - newDay.routineId = widget.routineId; - final result = await Provider.of(context, listen: false) - .addDay(newDay, refresh: true); - widget.onDaySelected(result.id!); + final day = Day.empty(); + day.name = i18n.newDay; + day.routineId = widget.routineId; + final newDay = await provider.addDay(day); + + // final newSlot = await provider.addSlot(Slot.withData( + // day: newDay.id, + // order: 1, + // )); + + widget.onDaySelected(newDay.id!); }, ), ], @@ -177,15 +186,32 @@ class _DayFormWidgetState extends State { @override Widget build(BuildContext context) { + final i18n = AppLocalizations.of(context); + return Form( key: _form, child: Column( children: [ Text( - widget.day.isRest ? 'REST DAY' : widget.day.name, + widget.day.isRest ? i18n.restDay : widget.day.name, style: Theme.of(context).textTheme.titleLarge, ), + SwitchListTile( + title: Text(i18n.isRestDay), + subtitle: Text(i18n.isRestDayHelp), + value: isRestDay, + contentPadding: const EdgeInsets.all(4), + onChanged: (value) { + setState(() { + isRestDay = value; + nameController.clear(); + descriptionController.clear(); + }); + widget.day.isRest = value; + }, + ), TextFormField( + enabled: !widget.day.isRest, key: const Key('field-name'), decoration: InputDecoration( labelText: AppLocalizations.of(context).name, @@ -195,6 +221,10 @@ class _DayFormWidgetState extends State { widget.day.name = value!; }, validator: (value) { + if (widget.day.isRest) { + return null; + } + if (value!.isEmpty || value.length < Day.MIN_LENGTH_NAME || value.length > Day.MAX_LENGTH_NAME) { @@ -207,19 +237,9 @@ class _DayFormWidgetState extends State { return null; }, ), - SwitchListTile( - title: const Text('is rest day'), - value: isRestDay, - contentPadding: const EdgeInsets.all(4), - onChanged: (value) { - setState(() { - isRestDay = value; - }); - widget.day.isRest = value; - }, - ), TextFormField( key: const Key('field-description'), + enabled: !widget.day.isRest, decoration: InputDecoration( labelText: AppLocalizations.of(context).description, helperText: AppLocalizations.of(context).dayDescriptionHelp, @@ -232,6 +252,10 @@ class _DayFormWidgetState extends State { minLines: 2, maxLines: 10, validator: (value) { + if (widget.day.isRest) { + return null; + } + if (value != null && value.length > Day.MAX_LENGTH_DESCRIPTION) { return AppLocalizations.of(context).enterCharacters(0, Day.MAX_LENGTH_DESCRIPTION); } @@ -239,6 +263,7 @@ class _DayFormWidgetState extends State { return null; }, ), + const SizedBox(height: 5), ElevatedButton( key: const Key(SUBMIT_BUTTON_KEY_NAME), child: Text(AppLocalizations.of(context).save), @@ -271,7 +296,8 @@ class _DayFormWidgetState extends State { } }, ), - ReorderableSlotList(widget.day.slots, widget.day.id!), + const SizedBox(height: 5), + ReorderableSlotList(widget.day.slots, widget.day), ], ), ); diff --git a/lib/widgets/routines/forms/routine.dart b/lib/widgets/routines/forms/routine.dart index 9e83fa23..8ec84966 100644 --- a/lib/widgets/routines/forms/routine.dart +++ b/lib/widgets/routines/forms/routine.dart @@ -43,19 +43,21 @@ class _RoutineFormState extends State { @override Widget build(BuildContext context) { + final i18n = AppLocalizations.of(context); + return Form( key: _form, child: Column( children: [ TextFormField( key: const Key('field-name'), - decoration: InputDecoration(labelText: AppLocalizations.of(context).name), + decoration: InputDecoration(labelText: i18n.name), controller: workoutNameController, validator: (value) { if (value!.isEmpty || value.length < Routine.MIN_LENGTH_NAME || value.length > Routine.MAX_LENGTH_NAME) { - return AppLocalizations.of(context).enterCharacters( + return i18n.enterCharacters( Routine.MIN_LENGTH_NAME, Routine.MAX_LENGTH_NAME, ); @@ -68,13 +70,13 @@ class _RoutineFormState extends State { ), TextFormField( key: const Key('field-description'), - decoration: InputDecoration(labelText: AppLocalizations.of(context).description), + decoration: InputDecoration(labelText: i18n.description), minLines: 3, maxLines: 10, controller: workoutDescriptionController, validator: (value) { if (value!.length > Routine.MAX_LENGTH_DESCRIPTION) { - return AppLocalizations.of(context).enterCharacters( + return i18n.enterCharacters( Routine.MIN_LENGTH_DESCRIPTION, Routine.MAX_LENGTH_DESCRIPTION, ); @@ -171,8 +173,11 @@ class _RoutineFormState extends State { } }, ), + const SizedBox(height: 5), SwitchListTile( - title: const Text('Fit in week'), + title: Text(i18n.fitInWeek), + subtitle: Text(i18n.fitInWeekHelp), + isThreeLine: true, value: widget._routine.fitInWeek, contentPadding: const EdgeInsets.all(4), onChanged: (bool? value) { @@ -188,6 +193,7 @@ class _RoutineFormState extends State { } }, ), + const SizedBox(height: 5), ElevatedButton( key: const Key(SUBMIT_BUTTON_KEY_NAME), child: Text(AppLocalizations.of(context).save), diff --git a/lib/widgets/routines/forms/slot.dart b/lib/widgets/routines/forms/slot.dart index b6f1073d..d635a5d1 100644 --- a/lib/widgets/routines/forms/slot.dart +++ b/lib/widgets/routines/forms/slot.dart @@ -20,6 +20,7 @@ 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/workouts/day.dart'; import 'package:wger/models/workouts/slot.dart'; import 'package:wger/models/workouts/slot_entry.dart'; import 'package:wger/providers/routines.dart'; @@ -35,6 +36,8 @@ class SlotEntryForm extends StatefulWidget { } class _SlotEntryFormState extends State { + final iconSize = 18.0; + final setsController = TextEditingController(); final weightController = TextEditingController(); final repsController = TextEditingController(); @@ -89,10 +92,12 @@ class _SlotEntryFormState extends State { _edit = !_edit; }); }, - icon: _edit ? const Icon(Icons.edit_off) : const Icon(Icons.edit), + icon: _edit + ? Icon(Icons.edit_off, size: iconSize) + : Icon(Icons.edit, size: iconSize), ), IconButton( - icon: const Icon(Icons.delete), + icon: Icon(Icons.delete, size: iconSize), onPressed: () { context.read().deleteSlotEntry(widget.entry.id!); }, @@ -111,7 +116,7 @@ class _SlotEntryFormState extends State { TextFormField( controller: setsController, keyboardType: TextInputType.number, - decoration: InputDecoration(labelText: 'Sets'), + decoration: InputDecoration(labelText: i18n.sets), ), TextFormField( controller: weightController, @@ -197,9 +202,9 @@ class _SlotDetailWidgetState extends State { class ReorderableSlotList extends StatefulWidget { final List slots; - final int dayId; + final Day day; - const ReorderableSlotList(this.slots, this.dayId); + const ReorderableSlotList(this.slots, this.day); @override _SlotFormWidgetStateNg createState() => _SlotFormWidgetStateNg(); @@ -217,23 +222,19 @@ class _SlotFormWidgetStateNg extends State { final languageCode = Localizations.localeOf(context).languageCode; - Slot? selectedSlot; - if (selectedSlotId != null) { - selectedSlot = widget.slots.firstWhere((slot) => slot.id == selectedSlotId); - } - return Column( children: [ - SwitchListTile( - value: simpleMode, - title: const Text('simple mode'), - contentPadding: const EdgeInsets.all(4), - onChanged: (value) { - setState(() { - simpleMode = value; - }); - }, - ), + if (!widget.day.isRest) + SwitchListTile( + value: simpleMode, + title: const Text('Simple edit mode'), + contentPadding: const EdgeInsets.all(4), + onChanged: (value) { + setState(() { + simpleMode = value; + }); + }, + ), ReorderableListView.builder( buildDefaultDragHandles: false, shrinkWrap: true, @@ -249,7 +250,8 @@ class _SlotFormWidgetStateNg extends State { child: Column( children: [ ListTile( - title: Text(i18n.setNr(index + 1)), + title: Text(slot.id.toString()), + // title: Text(i18n.setNr(index + 1)), tileColor: isCurrentSlotSelected ? Theme.of(context).highlightColor : null, leading: selectedSlotId == null ? ReorderableDragStartListener( @@ -314,22 +316,23 @@ class _SlotFormWidgetStateNg extends State { }); }, ), - ListTile( - leading: const Icon(Icons.add), - title: Text( - i18n.newSet, - style: Theme.of(context).textTheme.titleMedium, + if (!widget.day.isRest) + ListTile( + leading: const Icon(Icons.add), + title: Text( + i18n.addSet, + 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; + }); + }, ), - onTap: () async { - final newSlot = await provider.addSlot(Slot.withData( - day: widget.dayId, - order: widget.slots.length + 1, - )); - setState(() { - selectedSlotId = newSlot.id; - }); - }, - ), ], ); } diff --git a/lib/widgets/routines/routine_edit.dart b/lib/widgets/routines/routine_edit.dart index 7af7725f..2f60f821 100644 --- a/lib/widgets/routines/routine_edit.dart +++ b/lib/widgets/routines/routine_edit.dart @@ -16,9 +16,13 @@ * along with this program. If not, see . */ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:provider/provider.dart'; import 'package:wger/models/workouts/day.dart'; import 'package:wger/models/workouts/routine.dart'; +import 'package:wger/providers/routines.dart'; import 'package:wger/widgets/routines/forms/day.dart'; import 'package:wger/widgets/routines/forms/routine.dart'; import 'package:wger/widgets/routines/routine_detail.dart'; @@ -33,13 +37,26 @@ class RoutineEdit extends StatefulWidget { } class _RoutineEditState extends State { + late Future _dataFuture; int? selectedDayId; + @override + void initState() { + super.initState(); + _dataFuture = context + .read() + .fetchAndSetRoutineFull(widget._routine.id!); // Initialize the Future here + } + @override Widget build(BuildContext context) { + final i18n = AppLocalizations.of(context); + + final provider = context.read(); + Day? selectedDay; if (selectedDayId != null) { - selectedDay = widget._routine.days.firstWhere((day) => day.id == selectedDayId); + selectedDay = widget._routine.days.firstWhereOrNull((day) => day.id == selectedDayId); } return Padding( @@ -49,6 +66,10 @@ class _RoutineEditState extends State { children: [ RoutineForm(widget._routine), Container(height: 10), + Text( + i18n.routineDays, + style: Theme.of(context).textTheme.titleLarge, + ), ReorderableDaysList( routineId: widget._routine.id!, days: widget._routine.days, @@ -69,13 +90,37 @@ class _RoutineEditState extends State { day: selectedDay, routineId: widget._routine.id!, ), - const SizedBox(height: 50), + const SizedBox(height: 25), Text( - 'Resulting routine', + i18n.resultingRoutine, style: Theme.of(context).textTheme.titleLarge, ), + IconButton( + onPressed: () { + setState(() { + _dataFuture = provider.fetchAndSetRoutineFull(widget._routine.id!); + }); + }, + icon: const Icon(Icons.refresh), + ), const Divider(), - RoutineDetail(widget._routine), + FutureBuilder( + future: _dataFuture, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox( + height: 200, + child: Center( + child: CircularProgressIndicator(), + ), + ); + } else if (snapshot.hasData) { + return RoutineDetail(snapshot.data!); + } + return const Text('No data available'); + }, + ), + // RoutineDetail(widget._routine), ], ), ),