mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Show current workout in gym mode
This commit is contained in:
@@ -847,6 +847,7 @@
|
||||
"fitInWeek": "Fit in week",
|
||||
"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",
|
||||
"superset": "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.",
|
||||
"setHasNoExercises": "This set has no exercises yet!",
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:wger/helpers/shared_preferences.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/workouts/day_data.dart';
|
||||
import 'package:wger/models/workouts/set_config_data.dart';
|
||||
|
||||
part 'gym_state.g.dart';
|
||||
|
||||
@@ -39,6 +40,18 @@ class PageEntry {
|
||||
'SlotEntries can only be set for set pages',
|
||||
);
|
||||
|
||||
PageEntry copyWith({
|
||||
PageType? type,
|
||||
int? pageIndex,
|
||||
List<SlotPageEntry>? slotPages,
|
||||
}) {
|
||||
return PageEntry(
|
||||
type: type ?? this.type,
|
||||
pageIndex: pageIndex ?? this.pageIndex,
|
||||
slotPages: slotPages ?? this.slotPages,
|
||||
);
|
||||
}
|
||||
|
||||
List<int> get exerciseIds {
|
||||
final ids = <int>{};
|
||||
for (final entry in slotPages) {
|
||||
@@ -47,6 +60,10 @@ class PageEntry {
|
||||
return ids.toList();
|
||||
}
|
||||
|
||||
// Whether all sub-pages (e.g. log pages) are marked as done.
|
||||
bool get allLogsDone =>
|
||||
slotPages.where((entry) => entry.type == SlotPageType.log).every((entry) => entry.logDone);
|
||||
|
||||
@override
|
||||
String toString() => 'PageEntry(type: $type, pageIndex: $pageIndex)';
|
||||
}
|
||||
@@ -62,13 +79,39 @@ class SlotPageEntry {
|
||||
/// Absolute page index
|
||||
final int pageIndex;
|
||||
|
||||
const SlotPageEntry({
|
||||
/// Whether the log page has been marked as done
|
||||
final bool logDone;
|
||||
|
||||
/// The associated SetConfigData, only available for SlotPageType.log
|
||||
final SetConfigData? setConfigData;
|
||||
|
||||
SlotPageEntry({
|
||||
required this.type,
|
||||
required this.pageIndex,
|
||||
required this.exerciseId,
|
||||
required this.setIndex,
|
||||
this.setConfigData,
|
||||
this.logDone = false,
|
||||
});
|
||||
|
||||
SlotPageEntry copyWith({
|
||||
SlotPageType? type,
|
||||
int? exerciseId,
|
||||
int? setIndex,
|
||||
int? pageIndex,
|
||||
SetConfigData? setConfigData,
|
||||
bool? logDone,
|
||||
}) {
|
||||
return SlotPageEntry(
|
||||
type: type ?? this.type,
|
||||
exerciseId: exerciseId ?? this.exerciseId,
|
||||
setIndex: setIndex ?? this.setIndex,
|
||||
pageIndex: pageIndex ?? this.pageIndex,
|
||||
setConfigData: setConfigData ?? this.setConfigData,
|
||||
logDone: logDone ?? this.logDone,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'SlotEntry(type: $type, exerciseId: $exerciseId, setIndex: $setIndex, pageIndex: $pageIndex)';
|
||||
@@ -190,12 +233,12 @@ class GymStateNotifier extends _$GymStateNotifier {
|
||||
|
||||
void calculatePages(DayData dayData) {
|
||||
var totalPages = 1;
|
||||
final List<PageEntry> pages = [PageEntry(type: PageType.overview, pageIndex: totalPages - 1)];
|
||||
|
||||
final List<PageEntry> pages = [PageEntry(type: PageType.overview, pageIndex: 1)];
|
||||
|
||||
for (final slot in dayData.slots) {
|
||||
for (final slotData in dayData.slots) {
|
||||
final slotPageIndex = totalPages - 1;
|
||||
final slotEntries = <SlotPageEntry>[];
|
||||
int setIndex = 0;
|
||||
|
||||
// exercise overview page
|
||||
if (state.showExercisePages) {
|
||||
@@ -203,15 +246,14 @@ class GymStateNotifier extends _$GymStateNotifier {
|
||||
slotEntries.add(
|
||||
SlotPageEntry(
|
||||
type: SlotPageType.exerciseOverview,
|
||||
exerciseId: slot.setConfigs.first.exerciseId,
|
||||
setIndex: 0,
|
||||
exerciseId: slotData.setConfigs.first.exerciseId,
|
||||
setIndex: setIndex,
|
||||
pageIndex: totalPages - 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int setNr = 1;
|
||||
for (final config in slot.setConfigs) {
|
||||
for (final config in slotData.setConfigs) {
|
||||
// Timer page
|
||||
if (state.showTimerPages) {
|
||||
totalPages++;
|
||||
@@ -219,7 +261,7 @@ class GymStateNotifier extends _$GymStateNotifier {
|
||||
SlotPageEntry(
|
||||
type: SlotPageType.timer,
|
||||
exerciseId: config.exerciseId,
|
||||
setIndex: setNr,
|
||||
setIndex: setIndex,
|
||||
pageIndex: totalPages - 1,
|
||||
),
|
||||
);
|
||||
@@ -231,12 +273,13 @@ class GymStateNotifier extends _$GymStateNotifier {
|
||||
SlotPageEntry(
|
||||
type: SlotPageType.log,
|
||||
exerciseId: config.exerciseId,
|
||||
setIndex: setNr,
|
||||
setIndex: setIndex,
|
||||
pageIndex: totalPages - 1,
|
||||
setConfigData: config,
|
||||
),
|
||||
);
|
||||
|
||||
setNr++;
|
||||
setIndex++;
|
||||
}
|
||||
|
||||
pages.add(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 wger Team
|
||||
* Copyright (C) 2020, 2025 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
|
||||
@@ -15,6 +15,7 @@
|
||||
* 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 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -78,17 +79,16 @@ class _GymModeState extends ConsumerState<GymMode> {
|
||||
}
|
||||
|
||||
List<Widget> _getContent(GymState state) {
|
||||
final exerciseProvider = context.read<ExercisesProvider>();
|
||||
final routinesProvider = context.read<RoutinesProvider>();
|
||||
final List<Widget> out = [];
|
||||
|
||||
out.add(StartPage(_controller, widget._dayDataDisplay));
|
||||
|
||||
for (final slotData in widget._dayDataGym.slots) {
|
||||
var firstPage = true;
|
||||
for (final config in slotData.setConfigs) {
|
||||
final exercise = exerciseProvider.findExerciseById(config.exerciseId);
|
||||
|
||||
if (firstPage && state.showExercisePages) {
|
||||
out.add(ExerciseOverview(_controller, exercise));
|
||||
out.add(ExerciseOverview(_controller, config.exercise));
|
||||
}
|
||||
|
||||
out.add(
|
||||
@@ -96,7 +96,8 @@ class _GymModeState extends ConsumerState<GymMode> {
|
||||
_controller,
|
||||
config,
|
||||
slotData,
|
||||
exercise,
|
||||
widget._dayDataGym,
|
||||
config.exercise,
|
||||
routinesProvider.findById(widget._dayDataGym.day!.routineId),
|
||||
widget._iteration,
|
||||
),
|
||||
@@ -113,6 +114,16 @@ class _GymModeState extends ConsumerState<GymMode> {
|
||||
firstPage = false;
|
||||
}
|
||||
}
|
||||
|
||||
out.add(
|
||||
SessionPage(
|
||||
context.read<RoutinesProvider>().findById(widget._dayDataGym.day!.routineId),
|
||||
_controller,
|
||||
state.startTime,
|
||||
dayId: widget._dayDataGym.day!.id,
|
||||
),
|
||||
);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -138,14 +149,7 @@ class _GymModeState extends ConsumerState<GymMode> {
|
||||
final state = ref.watch(gymStateProvider);
|
||||
|
||||
final List<Widget> children = [
|
||||
StartPage(_controller, widget._dayDataDisplay),
|
||||
..._getContent(state),
|
||||
SessionPage(
|
||||
context.read<RoutinesProvider>().findById(widget._dayDataGym.day!.routineId),
|
||||
_controller,
|
||||
state.startTime,
|
||||
dayId: widget._dayDataGym.day!.id!,
|
||||
),
|
||||
];
|
||||
|
||||
return PageView(
|
||||
|
||||
@@ -24,6 +24,7 @@ import 'package:wger/exceptions/http_exception.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/workouts/day_data.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
import 'package:wger/models/workouts/routine.dart';
|
||||
import 'package:wger/models/workouts/set_config_data.dart';
|
||||
@@ -39,12 +40,32 @@ 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';
|
||||
import 'package:wger/widgets/routines/gym_mode/navigation.dart';
|
||||
import 'package:wger/widgets/routines/gym_mode/workout.dart';
|
||||
import 'package:wger/widgets/routines/plate_calculator.dart';
|
||||
|
||||
void _openWorkoutProgressionDialog(BuildContext context, DayData dayData) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
return AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).todaysWorkout),
|
||||
content: WorkoutProgression(dayData),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class LogPage extends ConsumerStatefulWidget {
|
||||
final PageController _controller;
|
||||
final SetConfigData _configData;
|
||||
final SlotData _slotData;
|
||||
final DayData _dayData;
|
||||
final Exercise _exercise;
|
||||
final Routine _routine;
|
||||
final Log _log;
|
||||
@@ -53,6 +74,7 @@ class LogPage extends ConsumerStatefulWidget {
|
||||
this._controller,
|
||||
this._configData,
|
||||
this._slotData,
|
||||
this._dayData,
|
||||
this._exercise,
|
||||
this._routine,
|
||||
int? iteration,
|
||||
@@ -121,6 +143,15 @@ class _LogPageState extends ConsumerState<LogPage> {
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_openWorkoutProgressionDialog(
|
||||
context,
|
||||
widget._dayData,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.menu_open),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -88,7 +88,7 @@ class NavigationHeader extends ConsumerWidget {
|
||||
Widget getDialog(BuildContext context, int totalPages, List<PageEntry> pages) {
|
||||
final exercisesProvider = context.read<ExercisesProvider>();
|
||||
|
||||
final TextButton? endWorkoutButton = showEndWorkoutButton
|
||||
final endWorkoutButton = showEndWorkoutButton
|
||||
? TextButton(
|
||||
child: Text(AppLocalizations.of(context).endWorkout),
|
||||
onPressed: () {
|
||||
@@ -125,8 +125,6 @@ class NavigationHeader extends ConsumerWidget {
|
||||
.toList()
|
||||
.join('\n'),
|
||||
),
|
||||
|
||||
//subtitle: e.exerciseIds.length > 1 ? Text('super set') : null,
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
_controller.animateToPage(
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2025 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_riverpod/flutter_riverpod.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
@@ -89,40 +107,32 @@ class StartPage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
..._dayData.slots.map((slotData) {
|
||||
return Column(
|
||||
children: [
|
||||
...slotData.setConfigs
|
||||
.fold<Map<Exercise, List<String>>>({}, (acc, entry) {
|
||||
acc.putIfAbsent(entry.exercise, () => []).add(entry.textReprWithType);
|
||||
return acc;
|
||||
})
|
||||
.entries
|
||||
.map((entry) {
|
||||
final exercise = entry.key;
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: SizedBox(
|
||||
width: 45,
|
||||
child: ExerciseImageWidget(image: exercise.getMainImage),
|
||||
),
|
||||
title: Text(
|
||||
exercise
|
||||
.getTranslation(Localizations.localeOf(context).languageCode)
|
||||
.name,
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: entry.value.map((text) => Text(text)).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}),
|
||||
..._dayData.slots
|
||||
.expand((slot) => slot.setConfigs)
|
||||
.fold<Map<Exercise, List<String>>>({}, (acc, entry) {
|
||||
acc.putIfAbsent(entry.exercise, () => []).add(entry.textReprWithType);
|
||||
return acc;
|
||||
})
|
||||
.entries
|
||||
.map((entry) {
|
||||
final exercise = entry.key;
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: SizedBox(
|
||||
width: 45,
|
||||
child: ExerciseImageWidget(image: exercise.getMainImage),
|
||||
),
|
||||
title: Text(
|
||||
exercise
|
||||
.getTranslation(Localizations.localeOf(context).languageCode)
|
||||
.name,
|
||||
),
|
||||
subtitle: Text(entry.value.toList().join('\n')),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
122
lib/widgets/routines/gym_mode/workout.dart
Normal file
122
lib/widgets/routines/gym_mode/workout.dart
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2025 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:logging/logging.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/workouts/day_data.dart';
|
||||
import 'package:wger/models/workouts/slot_data.dart';
|
||||
|
||||
class SlotDataRegular extends StatelessWidget {
|
||||
final _logger = Logger('SlotDataRegular');
|
||||
|
||||
final SlotData _slotData;
|
||||
|
||||
SlotDataRegular(this._slotData) : assert(!_slotData.isSuperset);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_slotData.setConfigs.isEmpty) {
|
||||
_logger.warning('Encountered an empty _slotData.setConfigs');
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final exercise = _slotData.setConfigs.first.exercise;
|
||||
return ListTile(
|
||||
title: Text(exercise.getTranslation(Localizations.localeOf(context).languageCode).name),
|
||||
subtitle: Text(_slotData.setConfigs.map((e) => e.textReprWithType).join('\n')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SlotDataSuperset extends StatelessWidget {
|
||||
final SlotData _slotData;
|
||||
|
||||
SlotDataSuperset(this._slotData) : assert(_slotData.isSuperset);
|
||||
|
||||
String _indexToLetters(int index) {
|
||||
final sb = StringBuffer();
|
||||
var i = index;
|
||||
while (i >= 0) {
|
||||
final rem = i % 26;
|
||||
sb.writeCharCode('A'.codeUnitAt(0) + rem);
|
||||
i = (i ~/ 26) - 1;
|
||||
}
|
||||
return sb.toString().split('').reversed.join();
|
||||
}
|
||||
|
||||
Map<int, String> _idsToLetters(List<int> ids) {
|
||||
final Map<int, String> result = {};
|
||||
final seen = <int>{};
|
||||
var counter = 0;
|
||||
for (final id in ids) {
|
||||
if (seen.add(id)) {
|
||||
result[id] = _indexToLetters(counter++);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Maps exercise IDs to letters (A, B, C, ...) so they can be referenced in the set configs
|
||||
final exerciseLetterMap = _idsToLetters(_slotData.exerciseIds);
|
||||
final exercises = _slotData.setConfigs.map((e) => e.exercise).toSet();
|
||||
|
||||
final exerciseText = exercises
|
||||
.map(
|
||||
(exercise) =>
|
||||
'${exerciseLetterMap[exercise.id!]}: ${exercise.getTranslation(Localizations.localeOf(context).languageCode).name}',
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
final repText = _slotData.setConfigs
|
||||
.map((e) {
|
||||
final letter = exerciseLetterMap[e.exercise.id]!;
|
||||
return '$letter: ${e.textReprWithType}';
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return ListTile(
|
||||
title: Text(AppLocalizations.of(context).superset),
|
||||
subtitle: Text(
|
||||
'$exerciseText \n\n$repText',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WorkoutProgression extends StatelessWidget {
|
||||
final DayData _dayData;
|
||||
|
||||
const WorkoutProgression(this._dayData);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
..._dayData.slots.map(
|
||||
(slotData) =>
|
||||
slotData.isSuperset ? SlotDataSuperset(slotData) : SlotDataRegular(slotData),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user