Some polishing, adding missing fields and other QoL

This commit is contained in:
Roland Geider
2025-01-05 19:45:02 +01:00
parent ad5d1be306
commit b66aa7444d
15 changed files with 176 additions and 153 deletions

View File

@@ -179,9 +179,17 @@
},
"dayDescriptionHelp": "A description of what is done on this day (e.g. 'pull day') or what body parts are trained (e.g. 'chest and shoulders')",
"@dayDescriptionHelp": {},
"setNr": "Set {nr}",
"@setNr": {
"description": "Header in form indicating the number of the current set. Can also be translated as something like 'Set Nr. xy'.",
"exerciseNr": "Exercise {nr}",
"@exerciseNr": {
"description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Set Nr. xy'.",
"type": "text",
"placeholders": {
"nr": {}
}
},
"supersetNr": "Superset {nr}",
"@supersetNr": {
"description": "Header in form indicating the number of the current exercise. Can also be translated as something like 'Superset Nr. xy'.",
"type": "text",
"placeholders": {
"nr": {}
@@ -207,7 +215,9 @@
},
"restDay": "Rest day",
"isRestDay": "Is rest day",
"isRestDayHelp": "Please consider that all sets are removed from rest days when the form is saved",
"isRestDayHelp": "Please note that all sets and exercises will be removed when you mark a day as a rest day.",
"needsLogsToAdvance": "Needs logs to advance",
"needsLogsToAdvanceHelp": "Select if you want the routine to progress to the next scheduled day only if you've logged a workout for the day",
"routineDays": "Days in routine",
"resultingRoutine": "Resulting routine",
"newDay": "New day",
@@ -707,7 +717,7 @@
"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.",
"fitInWeekHelp": "If enabled, the days will repeat in a weekly cycle, otherwise the days will follow sequentially without regards to the start of a new week.",
"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 automatic progression. For now, please use the web application.",
@@ -735,6 +745,7 @@
}
}
},
"progressionRules": "This exercise has progression rules and can't be edited on the mobile app. Please use the web application to edit this exercise.",
"cacheWarning": "Due to caching it might take some time till the changes are visible throughout the application.",
"textPromptTitle": "Ready to start?",
"textPromptSubheading": "Press the action button to begin",

View File

@@ -47,6 +47,9 @@ class BaseConfig {
@JsonKey(required: true, name: 'need_log_to_apply')
late bool needLogToApply;
@JsonKey(required: true, name: 'requirements')
late dynamic requirements;
BaseConfig({
required this.id,
required this.slotEntryId,
@@ -56,6 +59,7 @@ class BaseConfig {
required this.operation,
required this.step,
required this.needLogToApply,
required this.requirements,
});
BaseConfig.firstIteration(this.value, this.slotEntryId) {
@@ -63,6 +67,7 @@ class BaseConfig {
operation = 'r';
step = 'abs';
needLogToApply = false;
requirements = null;
}
// Boilerplate

View File

@@ -16,7 +16,8 @@ BaseConfig _$BaseConfigFromJson(Map<String, dynamic> json) {
'value',
'operation',
'step',
'need_log_to_apply'
'need_log_to_apply',
'requirements'
],
);
return BaseConfig(
@@ -27,6 +28,7 @@ BaseConfig _$BaseConfigFromJson(Map<String, dynamic> json) {
operation: json['operation'] as String,
step: json['step'] as String,
needLogToApply: json['need_log_to_apply'] as bool,
requirements: json['requirements'],
);
}
@@ -37,4 +39,5 @@ Map<String, dynamic> _$BaseConfigToJson(BaseConfig instance) => <String, dynamic
'operation': instance.operation,
'step': instance.step,
'need_log_to_apply': instance.needLogToApply,
'requirements': instance.requirements,
};

View File

@@ -93,9 +93,15 @@ class SlotEntry {
@JsonKey(required: false, name: 'set_nr_configs', includeToJson: false, defaultValue: [])
late List<BaseConfig> nrOfSetsConfigs = [];
@JsonKey(required: false, name: 'max_set_nr_configs', includeToJson: false, defaultValue: [])
late List<BaseConfig> maxNrOfSetsConfigs = [];
@JsonKey(required: false, name: 'rir_configs', includeToJson: false, defaultValue: [])
late List<BaseConfig> rirConfigs = [];
@JsonKey(required: false, name: 'max_rir_configs', includeToJson: false, defaultValue: [])
late List<BaseConfig> maxRirConfigs = [];
@JsonKey(required: false, name: 'rest_configs', includeToJson: false, defaultValue: [])
late List<BaseConfig> restTimeConfigs = [];
@@ -148,6 +154,20 @@ class SlotEntry {
return 'DELETE ME! RIR';
}
bool get hasProgressionRules {
return weightConfigs.length > 1 ||
repsConfigs.length > 1 ||
maxRepsConfigs.length > 1 ||
nrOfSetsConfigs.length > 1 ||
maxNrOfSetsConfigs.length > 1 ||
rirConfigs.length > 1 ||
maxRirConfigs.length > 1 ||
restTimeConfigs.length > 1 ||
maxRestTimeConfigs.length > 1 ||
maxWeightConfigs.length > 1 ||
maxWeightConfigs.length > 1;
}
List<BaseConfig> getConfigsByType(ConfigType type) {
switch (type) {
case ConfigType.weight:

View File

@@ -55,10 +55,18 @@ SlotEntry _$SlotEntryFromJson(Map<String, dynamic> json) {
?.map((e) => BaseConfig.fromJson(e as Map<String, dynamic>))
.toList() ??
[]
..maxNrOfSetsConfigs = (json['max_set_nr_configs'] as List<dynamic>?)
?.map((e) => BaseConfig.fromJson(e as Map<String, dynamic>))
.toList() ??
[]
..rirConfigs = (json['rir_configs'] as List<dynamic>?)
?.map((e) => BaseConfig.fromJson(e as Map<String, dynamic>))
.toList() ??
[]
..maxRirConfigs = (json['max_rir_configs'] as List<dynamic>?)
?.map((e) => BaseConfig.fromJson(e as Map<String, dynamic>))
.toList() ??
[]
..restTimeConfigs = (json['rest_configs'] as List<dynamic>?)
?.map((e) => BaseConfig.fromJson(e as Map<String, dynamic>))
.toList() ??

View File

@@ -510,10 +510,10 @@ class RoutinesProvider with ChangeNotifier {
notifyListeners();
}
Future<void> editSlot(Slot workoutSet) async {
Future<void> editSlot(Slot slot) async {
await baseProvider.patch(
workoutSet.toJson(),
baseProvider.makeUrl(_slotsUrlPath, id: workoutSet.id),
slot.toJson(),
baseProvider.makeUrl(_slotsUrlPath, id: slot.id),
);
notifyListeners();
}
@@ -565,6 +565,15 @@ class RoutinesProvider with ChangeNotifier {
notifyListeners();
}
Future<void> editSlotEntry(SlotEntry entry) async {
await baseProvider.patch(
entry.toJson(),
baseProvider.makeUrl(_slotEntriesUrlPath, id: entry.id),
);
notifyListeners();
}
String getConfigUrl(ConfigType type) {
switch (type) {
case ConfigType.sets:
@@ -624,45 +633,6 @@ class RoutinesProvider with ChangeNotifier {
}
}
Future<void> fetchComputedSettings(Slot slot) async {
final data = await baseProvider.fetch(
baseProvider.makeUrl(
_slotsUrlPath,
id: slot.id,
objectMethod: 'computed_settings',
),
);
final List<SlotEntry> settings = [];
data['results'].forEach((e) {
final SlotEntry workoutSetting = SlotEntry.fromJson(e);
workoutSetting.weightUnitObj = _weightUnits.firstWhere(
(unit) => unit.id == workoutSetting.weightUnitId,
);
workoutSetting.repetitionUnitObj = _repetitionUnit.firstWhere(
(unit) => unit.id == workoutSetting.repetitionUnitId,
);
settings.add(workoutSetting);
});
slot.settingsComputed = settings;
notifyListeners();
}
/*
* Setting
*/
Future<SlotEntry> addSetting(SlotEntry workoutSetting) async {
final data = await baseProvider.post(
workoutSetting.toJson(),
baseProvider.makeUrl(_slotEntriesUrlPath),
);
final setting = SlotEntry.fromJson(data);
notifyListeners();
return setting;
}
/*
* Sessions
*/

View File

@@ -72,8 +72,9 @@ class SetConfigDataWidget extends StatelessWidget {
class RoutineDayWidget extends StatelessWidget {
final DayData _dayData;
final int _routineId;
final bool _viewMode;
const RoutineDayWidget(this._dayData, this._routineId);
const RoutineDayWidget(this._dayData, this._routineId, this._viewMode);
Widget getSlotDataRow(SlotData slotData) {
return Column(
@@ -96,7 +97,7 @@ class RoutineDayWidget extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DayHeader(day: _dayData, routineId: _routineId),
DayHeader(day: _dayData, routineId: _routineId, viewMode: _viewMode),
..._dayData.slots.map((e) => getSlotDataRow(e)),
],
),
@@ -108,9 +109,11 @@ class RoutineDayWidget extends StatelessWidget {
class DayHeader extends StatelessWidget {
final DayData _dayData;
final int _routineId;
final bool _viewMode;
const DayHeader({required DayData day, required int routineId})
const DayHeader({required DayData day, required int routineId, bool viewMode = false})
: _dayData = day,
_viewMode = viewMode,
_routineId = routineId;
@override
@@ -141,13 +144,15 @@ class DayHeader extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
subtitle: Text(_dayData.day!.description),
leading: const Icon(Icons.play_arrow),
leading: _viewMode ? null : const Icon(Icons.play_arrow),
minLeadingWidth: 8,
onTap: () {
Navigator.of(context).pushNamed(
GymModeScreen.routeName,
arguments: GymModeArguments(_routineId, _dayData.day!.id!, _dayData.iteration),
);
_viewMode
? null
: Navigator.of(context).pushNamed(
GymModeScreen.routeName,
arguments: GymModeArguments(_routineId, _dayData.day!.id!, _dayData.iteration),
);
},
);
}

View File

@@ -55,7 +55,7 @@ class ExerciseSetting extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppLocalizations.of(context).setNr(i + 1),
AppLocalizations.of(context).exerciseNr(i + 1),
style: Theme.of(context).textTheme.titleLarge,
),
Row(
@@ -99,7 +99,7 @@ class ExerciseSetting extends StatelessWidget {
textBaseline: TextBaseline.alphabetic,
children: [
Text(
AppLocalizations.of(context).setNr(i + 1),
AppLocalizations.of(context).exerciseNr(i + 1),
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 10),

View File

@@ -164,6 +164,7 @@ class _DayFormWidgetState extends State<DayFormWidget> {
final descriptionController = TextEditingController();
final nameController = TextEditingController();
late bool isRestDay;
late bool needLogsToAdvance;
final _form = GlobalKey<FormState>();
@@ -171,6 +172,7 @@ class _DayFormWidgetState extends State<DayFormWidget> {
void initState() {
super.initState();
isRestDay = widget.day.isRest;
needLogsToAdvance = widget.day.needLogsToAdvance;
descriptionController.text = widget.day.description;
nameController.text = widget.day.name;
}
@@ -238,11 +240,7 @@ class _DayFormWidgetState extends State<DayFormWidget> {
TextFormField(
key: const Key('field-description'),
enabled: !widget.day.isRest,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).description,
helperText: AppLocalizations.of(context).dayDescriptionHelp,
helperMaxLines: 3,
),
decoration: InputDecoration(labelText: AppLocalizations.of(context).description),
controller: descriptionController,
onSaved: (value) {
widget.day.description = value!;
@@ -261,6 +259,18 @@ class _DayFormWidgetState extends State<DayFormWidget> {
return null;
},
),
SwitchListTile(
title: Text(i18n.needsLogsToAdvance),
subtitle: Text(i18n.needsLogsToAdvanceHelp),
value: needLogsToAdvance,
contentPadding: const EdgeInsets.all(4),
onChanged: (value) {
setState(() {
needLogsToAdvance = value;
});
widget.day.needLogsToAdvance = value;
},
),
const SizedBox(height: 5),
ElevatedButton(
key: const Key(SUBMIT_BUTTON_KEY_NAME),

View File

@@ -20,12 +20,44 @@ 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';
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;
@@ -176,6 +208,7 @@ class _SlotEntryFormState extends State<SlotEntryForm> {
repsController.text,
ConfigType.reps,
);
provider.editSlotEntry(widget.entry);
},
),
const SizedBox(height: 10),
@@ -204,7 +237,9 @@ class _SlotDetailWidgetState extends State<SlotDetailWidget> {
return Column(
children: [
...widget.slot.entries.map((entry) => SlotEntryForm(entry)),
...widget.slot.entries.map((entry) => entry.hasProgressionRules
? ProgressionRulesInfoBox(entry.exerciseObj)
: SlotEntryForm(entry)),
const SizedBox(height: 10),
if (_addSuperset || widget.slot.entries.isEmpty)
ExerciseAutocompleter(
@@ -219,13 +254,15 @@ class _SlotDetailWidgetState extends State<SlotDetailWidget> {
await provider.addSlotEntry(entry);
},
),
FilledButton(
if (widget.slot.entries.isNotEmpty)
FilledButton(
onPressed: () {
setState(() {
_addSuperset = !_addSuperset;
});
},
child: Text(i18n.addSuperset)),
child: Text(i18n.addSuperset),
),
const SizedBox(height: 5),
],
);
@@ -283,8 +320,7 @@ class _SlotFormWidgetStateNg extends State<ReorderableSlotList> {
child: Column(
children: [
ListTile(
// title: Text(slot.id.toString()),
title: Text(i18n.setNr(index + 1)),
title: Text(i18n.exerciseNr(index + 1)),
tileColor: isCurrentSlotSelected ? Theme.of(context).highlightColor : null,
leading: selectedSlotId == null
? ReorderableDragStartListener(
@@ -353,7 +389,7 @@ class _SlotFormWidgetStateNg extends State<ReorderableSlotList> {
ListTile(
leading: const Icon(Icons.add),
title: Text(
i18n.addSet,
i18n.addExercise,
style: Theme.of(context).textTheme.titleMedium,
),
onTap: () async {

View File

@@ -38,7 +38,7 @@ class RoutineDetail extends StatelessWidget {
),
..._routine.dayDataCurrentIteration
.where((dayData) => dayData.day != null)
.map((dayData) => RoutineDayWidget(dayData, _routine.id!)),
.map((dayData) => RoutineDayWidget(dayData, _routine.id!, viewMode)),
],
);
}

View File

@@ -72,7 +72,7 @@ class _RoutineEditState extends State<RoutineEdit> {
),
ReorderableDaysList(
routineId: widget._routine.id!,
days: widget._routine.days,
days: widget._routine.days.where((day) => day.id != null).toList(),
selectedDayId: selectedDayId,
onDaySelected: (id) {
setState(() {
@@ -115,7 +115,7 @@ class _RoutineEditState extends State<RoutineEdit> {
),
);
} else if (snapshot.hasData) {
return RoutineDetail(snapshot.data!);
return RoutineDetail(snapshot.data!, viewMode: true);
}
return const Text('No data available');
},

View File

@@ -471,10 +471,10 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
) as _i13.Future<void>);
@override
_i13.Future<void> editSlot(_i7.Slot? workoutSet) => (super.noSuchMethod(
_i13.Future<void> editSlot(_i7.Slot? slot) => (super.noSuchMethod(
Invocation.method(
#editSlot,
[workoutSet],
[slot],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
@@ -515,6 +515,16 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> editSlotEntry(_i8.SlotEntry? entry) => (super.noSuchMethod(
Invocation.method(
#editSlotEntry,
[entry],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
String getConfigUrl(_i8.ConfigType? type) => (super.noSuchMethod(
Invocation.method(
@@ -616,31 +626,6 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> fetchComputedSettings(_i7.Slot? slot) => (super.noSuchMethod(
Invocation.method(
#fetchComputedSettings,
[slot],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<_i8.SlotEntry> addSetting(_i8.SlotEntry? workoutSetting) => (super.noSuchMethod(
Invocation.method(
#addSetting,
[workoutSetting],
),
returnValue: _i13.Future<_i8.SlotEntry>.value(_FakeSlotEntry_6(
this,
Invocation.method(
#addSetting,
[workoutSetting],
),
)),
) as _i13.Future<_i8.SlotEntry>);
@override
_i13.Future<dynamic> fetchSessionData() => (super.noSuchMethod(
Invocation.method(

View File

@@ -471,10 +471,10 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
) as _i13.Future<void>);
@override
_i13.Future<void> editSlot(_i7.Slot? workoutSet) => (super.noSuchMethod(
_i13.Future<void> editSlot(_i7.Slot? slot) => (super.noSuchMethod(
Invocation.method(
#editSlot,
[workoutSet],
[slot],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
@@ -515,6 +515,16 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> editSlotEntry(_i8.SlotEntry? entry) => (super.noSuchMethod(
Invocation.method(
#editSlotEntry,
[entry],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
String getConfigUrl(_i8.ConfigType? type) => (super.noSuchMethod(
Invocation.method(
@@ -616,31 +626,6 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> fetchComputedSettings(_i7.Slot? slot) => (super.noSuchMethod(
Invocation.method(
#fetchComputedSettings,
[slot],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<_i8.SlotEntry> addSetting(_i8.SlotEntry? workoutSetting) => (super.noSuchMethod(
Invocation.method(
#addSetting,
[workoutSetting],
),
returnValue: _i13.Future<_i8.SlotEntry>.value(_FakeSlotEntry_6(
this,
Invocation.method(
#addSetting,
[workoutSetting],
),
)),
) as _i13.Future<_i8.SlotEntry>);
@override
_i13.Future<dynamic> fetchSessionData() => (super.noSuchMethod(
Invocation.method(

View File

@@ -471,10 +471,10 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
) as _i13.Future<void>);
@override
_i13.Future<void> editSlot(_i7.Slot? workoutSet) => (super.noSuchMethod(
_i13.Future<void> editSlot(_i7.Slot? slot) => (super.noSuchMethod(
Invocation.method(
#editSlot,
[workoutSet],
[slot],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
@@ -515,6 +515,16 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> editSlotEntry(_i8.SlotEntry? entry) => (super.noSuchMethod(
Invocation.method(
#editSlotEntry,
[entry],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
String getConfigUrl(_i8.ConfigType? type) => (super.noSuchMethod(
Invocation.method(
@@ -616,31 +626,6 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider {
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<void> fetchComputedSettings(_i7.Slot? slot) => (super.noSuchMethod(
Invocation.method(
#fetchComputedSettings,
[slot],
),
returnValue: _i13.Future<void>.value(),
returnValueForMissingStub: _i13.Future<void>.value(),
) as _i13.Future<void>);
@override
_i13.Future<_i8.SlotEntry> addSetting(_i8.SlotEntry? workoutSetting) => (super.noSuchMethod(
Invocation.method(
#addSetting,
[workoutSetting],
),
returnValue: _i13.Future<_i8.SlotEntry>.value(_FakeSlotEntry_6(
this,
Invocation.method(
#addSetting,
[workoutSetting],
),
)),
) as _i13.Future<_i8.SlotEntry>);
@override
_i13.Future<dynamic> fetchSessionData() => (super.noSuchMethod(
Invocation.method(