Allow adding additional exercises to the workout

While these are not real ad-hoc workouts, at least it's a first step
This commit is contained in:
Roland Geider
2025-11-15 23:44:42 +01:00
parent a072692ddd
commit c26e3828f8
4 changed files with 241 additions and 51 deletions

View File

@@ -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;

View File

@@ -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 = '',

View File

@@ -298,52 +298,57 @@ class GymStateNotifier extends _$GymStateNotifier {
/// Calculates the page entries
void calculatePages() {
var totalPages = 1;
final List<PageEntry> pages = [PageEntry(type: PageType.start, pageIndex: totalPages - 1)];
var pageIndex = 0;
final List<PageEntry> 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 = <SlotPageEntry>[];
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 = <PageEntry>[];
for (final page in state.pages) {
final slotPageIndex = pageIndex;
var setIndex = 0;
final updatedSlotPages = <SlotPageEntry>[];
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<PageEntry> pages = [];
for (final page in state.pages) {
pages.add(page);
if (page.uuid == pageEntryUUID) {
final setConfigData = page.slotPages.first.setConfigData!;
final List<SlotPageEntry> 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(

View File

@@ -113,8 +113,8 @@ class ProgressionTab extends ConsumerStatefulWidget {
}
class _ProgressionTabState extends ConsumerState<ProgressionTab> {
String? showDetailsForPageId;
String? showAddExerciseAfterPageId;
String? showSwapWidgetToPage;
String? showAddExerciseWidgetToPage;
_ProgressionTabState();
@override
@@ -177,7 +177,6 @@ class _ProgressionTabState extends ConsumerState<ProgressionTab> {
},
),
if (showDetailsForPageId == page.uuid) ExerciseSwapWidget(page.uuid),
Row(
mainAxisSize: MainAxisSize.max,
//mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -186,29 +185,46 @@ class _ProgressionTabState extends ConsumerState<ProgressionTab> {
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<ProgressionTab> {
),
],
),
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;