From c26e3828f89aff88448d9cf8350caf8986fb917d Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sat, 15 Nov 2025 23:44:42 +0100 Subject: [PATCH] Allow adding additional exercises to the workout While these are not real ad-hoc workouts, at least it's a first step --- lib/models/workouts/log.dart | 21 +++- lib/models/workouts/set_config_data.dart | 46 +++---- lib/providers/gym_state.dart | 117 ++++++++++++++++-- .../routines/gym_mode/workout_menu.dart | 108 ++++++++++++++-- 4 files changed, 241 insertions(+), 51 deletions(-) diff --git a/lib/models/workouts/log.dart b/lib/models/workouts/log.dart index f3ccfb81..e419911a 100644 --- a/lib/models/workouts/log.dart +++ b/lib/models/workouts/log.dart @@ -105,16 +105,25 @@ class Log { Log.fromSetConfigData(SetConfigData data) { date = DateTime.now(); sessionId = null; + slotEntryId = data.slotEntryId; exerciseBase = data.exercise; - weight = data.weight; - weightTarget = data.weight; - weightUnit = data.weightUnit; + if (data.weight != null) { + weight = data.weight; + weightTarget = data.weight; + } + if (data.weightUnit != null) { + weightUnit = data.weightUnit; + } - repetitions = data.repetitions; - repetitionsTarget = data.repetitions; - repetitionUnit = data.repetitionsUnit; + if (data.repetitions != null) { + repetitions = data.repetitions; + repetitionsTarget = data.repetitions; + } + if (data.repetitionsUnit != null) { + repetitionUnit = data.repetitionsUnit; + } rir = data.rir; rirTarget = data.rir; diff --git a/lib/models/workouts/set_config_data.dart b/lib/models/workouts/set_config_data.dart index ed66195a..38b8e152 100644 --- a/lib/models/workouts/set_config_data.dart +++ b/lib/models/workouts/set_config_data.dart @@ -46,55 +46,55 @@ class SetConfigData { String get textReprWithType => '$textRepr${type.typeLabel}'; @JsonKey(required: true, name: 'sets') - late num? nrOfSets; + num? nrOfSets; @JsonKey(required: true, name: 'max_sets') - late num? maxNrOfSets; + num? maxNrOfSets; @JsonKey(required: true, fromJson: stringToNumNull) - late num? weight; + num? weight; @JsonKey(required: true, name: 'max_weight', fromJson: stringToNumNull) - late num? maxWeight; + num? maxWeight; @JsonKey(required: true, name: 'weight_unit') - late int? weightUnitId; + int? weightUnitId; @JsonKey(includeToJson: false, includeFromJson: false) - late WeightUnit? weightUnit; + WeightUnit? weightUnit; @JsonKey(required: true, name: 'weight_rounding', fromJson: stringToNumNull) - late num? weightRounding; + num? weightRounding; @JsonKey(required: true, name: 'repetitions', fromJson: stringToNumNull) - late num? repetitions; + num? repetitions; @JsonKey(required: true, name: 'max_repetitions', fromJson: stringToNumNull) - late num? maxRepetitions; + num? maxRepetitions; @JsonKey(required: true, name: 'repetitions_unit') - late int? repetitionsUnitId; + int? repetitionsUnitId; @JsonKey(includeToJson: false, includeFromJson: false) - late RepetitionUnit? repetitionsUnit; + RepetitionUnit? repetitionsUnit; @JsonKey(required: true, name: 'repetitions_rounding', fromJson: stringToNumNull) - late num? repetitionsRounding; + num? repetitionsRounding; @JsonKey(required: true, fromJson: stringToNumNull) - late num? rir; + num? rir; @JsonKey(required: true, name: 'max_rir', fromJson: stringToNumNull) - late num? maxRir; + num? maxRir; @JsonKey(required: true, fromJson: stringToNumNull) - late num? rpe; + num? rpe; @JsonKey(required: true, name: 'rest', fromJson: stringToNumNull) - late num? restTime; + num? restTime; @JsonKey(required: true, name: 'max_rest', fromJson: stringToNumNull) - late num? maxRestTime; + num? maxRestTime; @JsonKey(required: true) late String comment; @@ -103,20 +103,20 @@ class SetConfigData { required this.exerciseId, required this.slotEntryId, this.type = SlotEntryType.normal, - required this.nrOfSets, + this.nrOfSets, this.maxNrOfSets, - required this.weight, + this.weight, this.maxWeight, this.weightUnitId = WEIGHT_UNIT_KG, this.weightRounding, - required this.repetitions, + this.repetitions, this.maxRepetitions, this.repetitionsUnitId = REP_UNIT_REPETITIONS_ID, this.repetitionsRounding, - required this.rir, + this.rir, this.maxRir, - required this.rpe, - required this.restTime, + this.rpe, + this.restTime, this.maxRestTime, this.comment = '', this.textRepr = '', diff --git a/lib/providers/gym_state.dart b/lib/providers/gym_state.dart index 22141d6f..b90d38b1 100644 --- a/lib/providers/gym_state.dart +++ b/lib/providers/gym_state.dart @@ -298,52 +298,57 @@ class GymStateNotifier extends _$GymStateNotifier { /// Calculates the page entries void calculatePages() { - var totalPages = 1; - final List pages = [PageEntry(type: PageType.start, pageIndex: totalPages - 1)]; + var pageIndex = 0; + final List pages = [ + // Start page + PageEntry(type: PageType.start, pageIndex: pageIndex), + ]; + + pageIndex++; for (final slotData in state.dayDataGym.slots) { - final slotPageIndex = totalPages; + final slotPageIndex = pageIndex; final slotEntries = []; int setIndex = 0; // exercise overview page if (state.showExercisePages) { - totalPages++; slotEntries.add( SlotPageEntry( type: SlotPageType.exerciseOverview, setIndex: setIndex, - pageIndex: totalPages - 1, + pageIndex: pageIndex, setConfigData: slotData.setConfigs.first, ), ); + pageIndex++; } for (final config in slotData.setConfigs) { // Timer page if (state.showTimerPages) { - totalPages++; slotEntries.add( SlotPageEntry( type: SlotPageType.timer, setIndex: setIndex, - pageIndex: totalPages - 1, + pageIndex: pageIndex, setConfigData: config, ), ); + pageIndex++; } // Log page - totalPages++; slotEntries.add( SlotPageEntry( type: SlotPageType.log, setIndex: setIndex, - pageIndex: totalPages - 1, + pageIndex: pageIndex, setConfigData: config, ), ); + pageIndex++; setIndex++; } @@ -357,13 +362,65 @@ class GymStateNotifier extends _$GymStateNotifier { } pages.add( - PageEntry(type: PageType.session, pageIndex: totalPages), + // Session page + PageEntry(type: PageType.session, pageIndex: pageIndex), ); state = state.copyWith(pages: pages); + debugStructure(); _logger.finer('Initialized ${state.pages.length} pages'); } + // Recalculates the indices of all pages + void recalculateIndices() { + var pageIndex = 0; + final updatedPages = []; + + for (final page in state.pages) { + final slotPageIndex = pageIndex; + var setIndex = 0; + final updatedSlotPages = []; + + for (final slotPage in page.slotPages) { + updatedSlotPages.add( + slotPage.copyWith( + pageIndex: pageIndex, + setIndex: setIndex, + ), + ); + setIndex++; + pageIndex++; + } + + if (page.type != PageType.set) { + pageIndex++; + } + + updatedPages.add( + page.copyWith( + pageIndex: slotPageIndex, + slotPages: updatedSlotPages, + ), + ); + } + + state = state.copyWith(pages: updatedPages); + debugStructure(); + _logger.fine('Recalculated page indices'); + } + + void debugStructure() { + _logger.fine('GymModeState structure:'); + for (final page in state.pages) { + _logger.fine('Page ${page.pageIndex}: ${page.type}'); + for (final slotPage in page.slotPages) { + _logger.fine( + ' SlotPage ${slotPage.pageIndex.toString().padLeft(2, ' ')} (set index ${slotPage.setIndex}): ${slotPage.type}', + ); + } + } + } + int initData(Routine routine, int dayId, int iteration) { final validUntil = state.validUntil; final currentPage = state.currentPage; @@ -470,6 +527,46 @@ class GymStateNotifier extends _$GymStateNotifier { _logger.fine('Replaced exercise $originalExerciseId with ${newExercise.id}'); } + void addExerciseAfterPage( + String pageEntryUUID, { + required Exercise newExercise, + }) { + final List pages = []; + for (final page in state.pages) { + pages.add(page); + + if (page.uuid == pageEntryUUID) { + final setConfigData = page.slotPages.first.setConfigData!; + + final List newSlotPages = []; + for (var i = 1; i <= 4; i++) { + newSlotPages.add( + SlotPageEntry( + type: SlotPageType.log, + pageIndex: 1, + setIndex: 0, + setConfigData: SetConfigData( + exerciseId: newExercise.id!, + exercise: newExercise, + slotEntryId: setConfigData.slotEntryId, + ), + ), + ); + } + + final newPage = PageEntry(type: PageType.set, pageIndex: 1, slotPages: newSlotPages); + + pages.add(newPage); + } + } + + state = state.copyWith( + pages: pages, + ); + + recalculateIndices(); + } + void clear() { _logger.fine('Clearing state'); state = state.copyWith( diff --git a/lib/widgets/routines/gym_mode/workout_menu.dart b/lib/widgets/routines/gym_mode/workout_menu.dart index d2d4a625..83768da4 100644 --- a/lib/widgets/routines/gym_mode/workout_menu.dart +++ b/lib/widgets/routines/gym_mode/workout_menu.dart @@ -113,8 +113,8 @@ class ProgressionTab extends ConsumerStatefulWidget { } class _ProgressionTabState extends ConsumerState { - String? showDetailsForPageId; - String? showAddExerciseAfterPageId; + String? showSwapWidgetToPage; + String? showAddExerciseWidgetToPage; _ProgressionTabState(); @override @@ -177,7 +177,6 @@ class _ProgressionTabState extends ConsumerState { }, ), - if (showDetailsForPageId == page.uuid) ExerciseSwapWidget(page.uuid), Row( mainAxisSize: MainAxisSize.max, //mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -186,29 +185,46 @@ class _ProgressionTabState extends ConsumerState { onPressed: page.allLogsDone ? null : () { - if (showDetailsForPageId == page.uuid) { + if (showSwapWidgetToPage == page.uuid) { setState(() { widget._logger.fine('Hiding details'); - showDetailsForPageId = null; + showSwapWidgetToPage = null; }); } else { setState(() { widget._logger.fine('Showing details for page ${page.uuid}'); - showDetailsForPageId = page.uuid; + showSwapWidgetToPage = page.uuid; + showAddExerciseWidgetToPage = null; }); } }, icon: Icon( key: ValueKey('swap-icon-${page.uuid}'), - showDetailsForPageId == page.uuid + showSwapWidgetToPage == page.uuid ? Icons.change_circle : Icons.change_circle_outlined, ), ), - // IconButton( - // onPressed: () {}, - // icon: const Icon(Icons.add), - // ), + IconButton( + onPressed: page.allLogsDone + ? null + : () { + if (showAddExerciseWidgetToPage == page.uuid) { + setState(() { + showAddExerciseWidgetToPage = null; + }); + } else { + setState(() { + showAddExerciseWidgetToPage = page.uuid; + showSwapWidgetToPage = null; + }); + } + }, + icon: Icon( + key: ValueKey('add-icon-${page.uuid}'), + showAddExerciseWidgetToPage == page.uuid ? Icons.add_circle : Icons.add, + ), + ), Expanded(child: Container()), IconButton( onPressed: () { @@ -223,6 +239,24 @@ class _ProgressionTabState extends ConsumerState { ), ], ), + if (showSwapWidgetToPage == page.uuid) + ExerciseSwapWidget( + page.uuid, + onDone: () { + setState(() { + showSwapWidgetToPage = null; + }); + }, + ), + if (showAddExerciseWidgetToPage == page.uuid) + ExerciseAddWidget( + page.uuid, + onDone: () { + setState(() { + showAddExerciseWidgetToPage = null; + }); + }, + ), const SizedBox(height: 8), ], ); @@ -246,8 +280,9 @@ class ExerciseSwapWidget extends ConsumerWidget { final _logger = Logger('ExerciseSwapWidget'); final String pageUUID; + final VoidCallback? onDone; - ExerciseSwapWidget(this.pageUUID, {super.key}); + ExerciseSwapWidget(this.pageUUID, {this.onDone, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -278,6 +313,7 @@ class ExerciseSwapWidget extends ConsumerWidget { originalExerciseId: e.id!, newExercise: exercise, ); + onDone?.call(); _logger.fine('Replaced exercise ${e.id} with ${exercise.id}'); }, ), @@ -293,6 +329,54 @@ class ExerciseSwapWidget extends ConsumerWidget { } } +class ExerciseAddWidget extends ConsumerWidget { + final _logger = Logger('ExerciseAddWidget'); + + final String pageUUID; + final VoidCallback? onDone; + + ExerciseAddWidget(this.pageUUID, {this.onDone, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(gymStateProvider); + final gymProvider = ref.read(gymStateProvider.notifier); + final page = state.pages.firstWhere((p) => p.uuid == pageUUID); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Card( + child: Padding( + padding: const EdgeInsets.all(5), + child: Column( + children: [ + ...page.exercises.map((e) { + return Column( + mainAxisSize: MainAxisSize.max, + children: [ + ExerciseAutocompleter( + onExerciseSelected: (exercise) { + gymProvider.addExerciseAfterPage( + page.uuid, + newExercise: exercise, + ); + onDone?.call(); + _logger.fine('Added exercise ${exercise.id} after page $pageUUID'); + }, + ), + const Icon(Icons.arrow_downward), + const SizedBox(height: 10), + ], + ); + }), + ], + ), + ), + ), + ); + } +} + class WorkoutMenuDialog extends ConsumerWidget { final PageController controller; final bool showEndWorkoutButton;