mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-19 07:50:52 +01:00
Allow the user to control the gym mode
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/workouts/day_data.dart';
|
||||
|
||||
part 'gym_state.g.dart';
|
||||
|
||||
@@ -11,27 +12,34 @@ const DEFAULT_DURATION = Duration(hours: 5);
|
||||
class GymState {
|
||||
final Map<Exercise, int> exercisePages;
|
||||
final bool showExercisePages;
|
||||
final bool showTimerPages;
|
||||
final int currentPage;
|
||||
final int totalPages;
|
||||
final int totalElements;
|
||||
final int? dayId;
|
||||
late TimeOfDay startTime;
|
||||
late DateTime validUntil;
|
||||
final TimeOfDay startTime;
|
||||
final DateTime validUntil;
|
||||
|
||||
GymState({
|
||||
this.exercisePages = const {},
|
||||
this.showExercisePages = true,
|
||||
this.showTimerPages = true,
|
||||
this.currentPage = 0,
|
||||
this.totalPages = 1,
|
||||
this.totalElements = 1,
|
||||
this.dayId,
|
||||
DateTime? validUntil,
|
||||
TimeOfDay? startTime,
|
||||
}) {
|
||||
this.validUntil = validUntil ?? clock.now().add(DEFAULT_DURATION);
|
||||
this.startTime = startTime ?? TimeOfDay.fromDateTime(clock.now());
|
||||
}
|
||||
}) : validUntil = validUntil ?? clock.now().add(DEFAULT_DURATION),
|
||||
startTime = startTime ?? TimeOfDay.fromDateTime(clock.now());
|
||||
|
||||
GymState copyWith({
|
||||
Map<Exercise, int>? exercisePages,
|
||||
bool? showExercisePages,
|
||||
bool? showTimerPages,
|
||||
int? currentPage,
|
||||
int? totalPages,
|
||||
int? totalElements,
|
||||
int? dayId,
|
||||
DateTime? validUntil,
|
||||
TimeOfDay? startTime,
|
||||
@@ -39,7 +47,10 @@ class GymState {
|
||||
return GymState(
|
||||
exercisePages: exercisePages ?? this.exercisePages,
|
||||
showExercisePages: showExercisePages ?? this.showExercisePages,
|
||||
showTimerPages: showTimerPages ?? this.showTimerPages,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
totalPages: totalPages ?? this.totalPages,
|
||||
totalElements: totalElements ?? this.totalElements,
|
||||
dayId: dayId ?? this.dayId,
|
||||
validUntil: validUntil ?? this.validUntil,
|
||||
startTime: startTime ?? this.startTime,
|
||||
@@ -50,51 +61,123 @@ class GymState {
|
||||
String toString() {
|
||||
return 'GymState('
|
||||
'currentPage: $currentPage, '
|
||||
'showExercisePages: $showExercisePages, '
|
||||
'totalPages: $totalPages, '
|
||||
'totalElements: $totalElements, '
|
||||
'exercisePages: ${exercisePages.length} exercises, '
|
||||
'dayId: $dayId, '
|
||||
'validUntil: $validUntil '
|
||||
'startTime: $startTime, '
|
||||
'showExercisePages: $showExercisePages, '
|
||||
'showTimerPages: $showTimerPages, '
|
||||
')';
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
@Riverpod(keepAlive: true)
|
||||
class GymStateNotifier extends _$GymStateNotifier {
|
||||
final _logger = Logger('GymStateNotifier');
|
||||
|
||||
@override
|
||||
GymState build() => GymState();
|
||||
GymState build() {
|
||||
_logger.finer('Initializing GymStateNotifier with default state');
|
||||
return GymState();
|
||||
}
|
||||
|
||||
void computePagesForDay(DayData dayData, Exercise Function(int) findExerciseById) {
|
||||
var totalElements = 1;
|
||||
var totalPages = 1;
|
||||
final Map<Exercise, int> exercisePages = {};
|
||||
|
||||
for (final slot in dayData.slots) {
|
||||
totalElements += slot.setConfigs.length;
|
||||
// exercise overview page
|
||||
if (state.showExercisePages) {
|
||||
totalPages += 1;
|
||||
}
|
||||
|
||||
for (final config in slot.setConfigs) {
|
||||
// log + timer per set
|
||||
if (state.showTimerPages) {
|
||||
totalPages += (config.nrOfSets! * 2).toInt();
|
||||
} else {
|
||||
totalPages += config.nrOfSets!.toInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var currentPage = 1;
|
||||
for (final slot in dayData.slots) {
|
||||
var firstPage = true;
|
||||
for (final config in slot.setConfigs) {
|
||||
final exercise = findExerciseById(config.exerciseId);
|
||||
if (firstPage) {
|
||||
exercisePages[exercise] = currentPage;
|
||||
currentPage++;
|
||||
}
|
||||
currentPage += 2;
|
||||
firstPage = false;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.finer('Total pages: $totalPages');
|
||||
_logger.finer('Total elements: $totalElements');
|
||||
|
||||
state = state.copyWith(
|
||||
exercisePages: exercisePages,
|
||||
totalPages: totalPages,
|
||||
totalElements: totalElements,
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> initForDay(
|
||||
DayData dayData, {
|
||||
required Exercise Function(int) findExerciseById,
|
||||
}) async {
|
||||
final newDayId = dayData.day!.id!;
|
||||
final validUntil = state.validUntil;
|
||||
final currentPage = state.currentPage;
|
||||
final savedDayId = state.dayId;
|
||||
|
||||
final shouldReset = newDayId != savedDayId || validUntil.isBefore(DateTime.now());
|
||||
if (shouldReset) {
|
||||
_logger.fine('Day ID mismatch or expired validUntil date. Resetting to page 0.');
|
||||
}
|
||||
final initialPage = shouldReset ? 0 : currentPage;
|
||||
|
||||
// compute pages (pure, uses callback for exercise lookup)
|
||||
computePagesForDay(dayData, findExerciseById);
|
||||
|
||||
// set dayId and initial page
|
||||
state = state.copyWith(
|
||||
dayId: newDayId,
|
||||
currentPage: initialPage,
|
||||
);
|
||||
|
||||
return initialPage;
|
||||
}
|
||||
|
||||
void setCurrentPage(int page) {
|
||||
// _logger.fine('Setting page from ${state.currentPage} to $page');
|
||||
state = state.copyWith(currentPage: page);
|
||||
}
|
||||
|
||||
void toggleExercisePages() {
|
||||
state = state.copyWith(showExercisePages: !state.showExercisePages);
|
||||
void setShowExercisePages(bool value) {
|
||||
state = state.copyWith(showExercisePages: value);
|
||||
}
|
||||
|
||||
void setShowTimerPages(bool value) {
|
||||
state = state.copyWith(showTimerPages: value);
|
||||
}
|
||||
|
||||
void setDayId(int dayId) {
|
||||
// _logger.fine('Setting day id from ${state.dayId} to $dayId');
|
||||
state = state.copyWith(dayId: dayId);
|
||||
}
|
||||
|
||||
void setExercisePages(Map<Exercise, int> exercisePages) {
|
||||
// _logger.fine('Setting exercise pages - ${exercisePages.length} exercises');
|
||||
state = state.copyWith(exercisePages: exercisePages);
|
||||
// _logger.fine(
|
||||
// 'Exercise pages set - ${exercisePages.entries.map((e) => '${e.key.id}: ${e.value}').join(', ')}');
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_logger.fine('Clearing state');
|
||||
state = state.copyWith(
|
||||
exercisePages: {},
|
||||
currentPage: 0,
|
||||
dayId: null,
|
||||
validUntil: DateTime.now().add(DEFAULT_DURATION),
|
||||
startTime: TimeOfDay.now(),
|
||||
);
|
||||
state = GymState();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ final class GymStateNotifierProvider extends $NotifierProvider<GymStateNotifier,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'gymStateProvider',
|
||||
isAutoDispose: true,
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
@@ -40,7 +40,7 @@ final class GymStateNotifierProvider extends $NotifierProvider<GymStateNotifier,
|
||||
}
|
||||
}
|
||||
|
||||
String _$gymStateNotifierHash() => r'bb6bdc27f41e052312a159dd9bfca0873c474c59';
|
||||
String _$gymStateNotifierHash() => r'ee943c23a68e678830c65a0c53bfd609feb6bf62';
|
||||
|
||||
abstract class _$GymStateNotifier extends $Notifier<GymState> {
|
||||
GymState build();
|
||||
|
||||
@@ -21,7 +21,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart' as provider;
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/workouts/day_data.dart';
|
||||
import 'package:wger/providers/exercises.dart';
|
||||
import 'package:wger/providers/gym_state.dart';
|
||||
@@ -45,101 +44,53 @@ class GymMode extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _GymModeState extends ConsumerState<GymMode> {
|
||||
var _totalElements = 1;
|
||||
var _totalPages = 1;
|
||||
late Future<int> _initData;
|
||||
bool _initialPageJumped = false;
|
||||
|
||||
/// Map with the first (navigation) page for each exercise
|
||||
final Map<Exercise, int> _exercisePages = {};
|
||||
late final PageController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = PageController(initialPage: 0);
|
||||
_initData = _loadGymState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initData = _loadGymState();
|
||||
_controller = PageController(initialPage: 0);
|
||||
_calculatePages();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(gymStateProvider.notifier).setExercisePages(_exercisePages);
|
||||
});
|
||||
}
|
||||
|
||||
Future<int> _loadGymState() async {
|
||||
// Re-fetch the current routine data to ensure we have the latest session
|
||||
// data since it is possible that the user created or deleted it from the
|
||||
// web interface.
|
||||
await context.read<RoutinesProvider>().fetchAndSetRoutineFull(
|
||||
await provider.Provider.of<RoutinesProvider>(context, listen: false).fetchAndSetRoutineFull(
|
||||
widget._dayDataGym.day!.routineId,
|
||||
);
|
||||
widget._logger.fine('Refreshed routine data');
|
||||
|
||||
final validUntil = ref.read(gymStateProvider).validUntil;
|
||||
final currentPage = ref.read(gymStateProvider).currentPage;
|
||||
final savedDayId = ref.read(gymStateProvider).dayId;
|
||||
final newDayId = widget._dayDataGym.day!.id!;
|
||||
|
||||
final shouldReset = newDayId != savedDayId || validUntil.isBefore(DateTime.now());
|
||||
if (shouldReset) {
|
||||
widget._logger.fine('Day ID mismatch or expired validUntil date. Resetting to page 0.');
|
||||
}
|
||||
final initialPage = shouldReset ? 0 : currentPage;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(gymStateProvider.notifier)
|
||||
..setDayId(newDayId)
|
||||
..setCurrentPage(initialPage);
|
||||
});
|
||||
final gymProvider = ref.read(gymStateProvider.notifier);
|
||||
final initialPage = await gymProvider.initForDay(
|
||||
widget._dayDataGym,
|
||||
findExerciseById: (id) =>
|
||||
provider.Provider.of<ExercisesProvider>(context, listen: false).findExerciseById(id),
|
||||
);
|
||||
|
||||
return initialPage;
|
||||
}
|
||||
|
||||
void _calculatePages() {
|
||||
for (final slot in widget._dayDataGym.slots) {
|
||||
_totalElements += slot.setConfigs.length;
|
||||
// add 1 for each exercise
|
||||
_totalPages += 1;
|
||||
for (final config in slot.setConfigs) {
|
||||
// add nrOfSets * 2, 1 for log page and 1 for timer
|
||||
_totalPages += (config.nrOfSets! * 2).toInt();
|
||||
}
|
||||
}
|
||||
_exercisePages.clear();
|
||||
var currentPage = 1;
|
||||
|
||||
for (final slot in widget._dayDataGym.slots) {
|
||||
var firstPage = true;
|
||||
for (final config in slot.setConfigs) {
|
||||
final exercise = context.read<ExercisesProvider>().findExerciseById(config.exerciseId);
|
||||
|
||||
if (firstPage) {
|
||||
_exercisePages[exercise] = currentPage;
|
||||
currentPage++;
|
||||
}
|
||||
currentPage += 2;
|
||||
firstPage = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> getContent() {
|
||||
final state = ref.watch(gymStateProvider);
|
||||
final exerciseProvider = context.read<ExercisesProvider>();
|
||||
final routinesProvider = context.read<RoutinesProvider>();
|
||||
List<Widget> _getContent(GymState state) {
|
||||
final exerciseProvider = provider.Provider.of<ExercisesProvider>(context, listen: false);
|
||||
final routinesProvider = provider.Provider.of<RoutinesProvider>(context, listen: false);
|
||||
var currentElement = 1;
|
||||
final List<Widget> out = [];
|
||||
|
||||
final totalElements = state.totalElements;
|
||||
final totalPages = state.totalPages;
|
||||
final exercisePages = state.exercisePages;
|
||||
|
||||
for (final slotData in widget._dayDataGym.slots) {
|
||||
var firstPage = true;
|
||||
for (final config in slotData.setConfigs) {
|
||||
final ratioCompleted = currentElement / _totalElements;
|
||||
final ratioCompleted = currentElement / totalElements;
|
||||
final exercise = exerciseProvider.findExerciseById(config.exerciseId);
|
||||
currentElement++;
|
||||
|
||||
@@ -149,8 +100,8 @@ class _GymModeState extends ConsumerState<GymMode> {
|
||||
_controller,
|
||||
exercise,
|
||||
ratioCompleted,
|
||||
state.exercisePages,
|
||||
_totalPages,
|
||||
exercisePages,
|
||||
totalPages,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -163,25 +114,26 @@ class _GymModeState extends ConsumerState<GymMode> {
|
||||
exercise,
|
||||
routinesProvider.findById(widget._dayDataGym.day!.routineId),
|
||||
ratioCompleted,
|
||||
state.exercisePages,
|
||||
_totalPages,
|
||||
exercisePages,
|
||||
totalPages,
|
||||
widget._iteration,
|
||||
),
|
||||
);
|
||||
|
||||
// If there is a rest time, add a countdown timer
|
||||
if (config.restTime != null) {
|
||||
out.add(
|
||||
TimerCountdownWidget(
|
||||
_controller,
|
||||
config.restTime!.toInt(),
|
||||
ratioCompleted,
|
||||
state.exercisePages,
|
||||
_totalPages,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
out.add(TimerWidget(_controller, ratioCompleted, state.exercisePages, _totalPages));
|
||||
if (state.showTimerPages) {
|
||||
if (config.restTime != null) {
|
||||
out.add(
|
||||
TimerCountdownWidget(
|
||||
_controller,
|
||||
config.restTime!.toInt(),
|
||||
ratioCompleted,
|
||||
exercisePages,
|
||||
totalPages,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
out.add(TimerWidget(_controller, ratioCompleted, exercisePages, totalPages));
|
||||
}
|
||||
}
|
||||
|
||||
firstPage = false;
|
||||
@@ -209,14 +161,16 @@ class _GymModeState extends ConsumerState<GymMode> {
|
||||
}
|
||||
});
|
||||
|
||||
final state = ref.watch(gymStateProvider);
|
||||
|
||||
final List<Widget> children = [
|
||||
StartPage(_controller, widget._dayDataDisplay, _exercisePages),
|
||||
...getContent(),
|
||||
StartPage(_controller, widget._dayDataDisplay, state.exercisePages),
|
||||
..._getContent(state),
|
||||
SessionPage(
|
||||
context.read<RoutinesProvider>().findById(widget._dayDataGym.day!.routineId),
|
||||
_controller,
|
||||
ref.read(gymStateProvider).startTime,
|
||||
_exercisePages,
|
||||
state.startTime,
|
||||
state.exercisePages,
|
||||
dayId: widget._dayDataGym.day!.id!,
|
||||
),
|
||||
];
|
||||
@@ -228,6 +182,7 @@ class _GymModeState extends ConsumerState<GymMode> {
|
||||
|
||||
// Check if the last page is reached
|
||||
if (page == children.length - 1) {
|
||||
widget._logger.finer('Last page reached, clearing gym state');
|
||||
ref.read(gymStateProvider.notifier).clear();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,12 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/workouts/day.dart';
|
||||
import 'package:wger/models/workouts/day_data.dart';
|
||||
import 'package:wger/providers/gym_state.dart';
|
||||
import 'package:wger/widgets/exercises/images.dart';
|
||||
import 'package:wger/widgets/routines/gym_mode/navigation.dart';
|
||||
|
||||
class StartPage extends StatelessWidget {
|
||||
class GymModeOptions extends ConsumerStatefulWidget {
|
||||
const GymModeOptions({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<GymModeOptions> createState() => _GymModeOptionsState();
|
||||
}
|
||||
|
||||
class _GymModeOptionsState extends ConsumerState<GymModeOptions> {
|
||||
bool _showOptions = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final gymState = ref.watch(gymStateProvider);
|
||||
final gymNotifier = ref.watch(gymStateProvider.notifier);
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
AnimatedCrossFade(
|
||||
firstChild: const SizedBox.shrink(),
|
||||
secondChild: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text('Show exercise overview pages'),
|
||||
value: gymState.showExercisePages,
|
||||
onChanged: (value) => gymNotifier.setShowExercisePages(value),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Show timer'),
|
||||
value: gymState.showTimerPages,
|
||||
onChanged: (value) => gymNotifier.setShowTimerPages(value),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
crossFadeState: _showOptions ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
),
|
||||
|
||||
ListTile(
|
||||
title: Text(i18n.settingsTitle),
|
||||
leading: const Icon(Icons.settings),
|
||||
onTap: () => setState(() => _showOptions = !_showOptions),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class StartPage extends ConsumerWidget {
|
||||
final PageController _controller;
|
||||
final DayData _dayData;
|
||||
final Map<Exercise, int> _exercisePages;
|
||||
@@ -14,7 +67,7 @@ class StartPage extends StatelessWidget {
|
||||
const StartPage(this._controller, this._dayData, this._exercisePages);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Column(
|
||||
children: [
|
||||
NavigationHeader(
|
||||
@@ -22,6 +75,7 @@ class StartPage extends StatelessWidget {
|
||||
_controller,
|
||||
exercisePages: _exercisePages,
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
@@ -73,6 +127,7 @@ class StartPage extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const GymModeOptions(),
|
||||
FilledButton(
|
||||
child: Text(AppLocalizations.of(context).start),
|
||||
onPressed: () {
|
||||
|
||||
Reference in New Issue
Block a user