Allow some options for the user to control the timer between sets

This commit is contained in:
Roland Geider
2025-12-02 22:32:14 +01:00
parent 55259e7483
commit ffc2f1a674
6 changed files with 257 additions and 86 deletions

View File

@@ -79,6 +79,7 @@ class _GymModeState extends ConsumerState<GymMode> {
}
List<Widget> _getContent(GymModeState state) {
final gymState = ref.watch(gymStateProvider);
final List<Widget> out = [];
// Workout overview
@@ -95,12 +96,17 @@ class _GymModeState extends ConsumerState<GymMode> {
out.add(LogPage(_controller));
}
// Timer. Use rest time from config data if available, otherwise use user settings
final rest = slotPage.setConfigData?.restTime;
if (slotPage.type == SlotPageType.timer) {
if (slotPage.setConfigData!.restTime != null) {
out.add(TimerCountdownWidget(_controller, slotPage.setConfigData!.restTime!.toInt()));
} else {
out.add(TimerWidget(_controller));
}
out.add(
(rest != null || gymState.useCountdownBetweenSets)
? TimerCountdownWidget(
_controller,
(rest ?? gymState.defaultCountdownDuration.inSeconds).toInt(),
)
: TimerWidget(_controller),
);
}
}
}
@@ -144,7 +150,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();
ref.read(gymStateProvider.notifier).clear();
}
},
children: children,

View File

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

View File

@@ -34,6 +34,20 @@ class GymModeOptions extends ConsumerStatefulWidget {
class _GymModeOptionsState extends ConsumerState<GymModeOptions> {
bool _showOptions = false;
late TextEditingController _countdownController;
@override
void initState() {
super.initState();
final initial = ref.read(gymStateProvider).defaultCountdownDuration.inSeconds.toString();
_countdownController = TextEditingController(text: initial);
}
@override
void dispose() {
_countdownController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
@@ -41,27 +55,117 @@ class _GymModeOptionsState extends ConsumerState<GymModeOptions> {
final gymNotifier = ref.watch(gymStateProvider.notifier);
final i18n = AppLocalizations.of(context);
// If the value in the provider changed, update the controller text
final currentText = gymState.defaultCountdownDuration.inSeconds.toString();
if (_countdownController.text != currentText) {
_countdownController.text = currentText;
}
return Column(
children: [
AnimatedCrossFade(
firstChild: const SizedBox.shrink(),
secondChild: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Column(
children: [
SwitchListTile(
key: const ValueKey('gym-mode-option-show-exercises'),
title: Text(i18n.gymModeShowExercises),
value: gymState.showExercisePages,
onChanged: (value) => gymNotifier.setShowExercisePages(value),
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 400),
child: Card(
child: SingleChildScrollView(
child: Column(
children: [
SwitchListTile(
key: const ValueKey('gym-mode-option-show-exercises'),
title: Text(i18n.gymModeShowExercises),
value: gymState.showExercisePages,
onChanged: (value) => gymNotifier.setShowExercisePages(value),
),
SwitchListTile(
key: const ValueKey('gym-mode-option-show-timer'),
title: Text(i18n.gymModeShowTimer),
value: gymState.showTimerPages,
onChanged: (value) => gymNotifier.setShowTimerPages(value),
),
ListTile(
enabled: gymState.showTimerPages,
title: Text(i18n.gymModeTimerType),
trailing: DropdownButton<bool>(
key: const ValueKey('themeModeDropdown'),
value: gymState.useCountdownBetweenSets,
onChanged: gymState.showTimerPages
? (bool? newValue) {
if (newValue != null) {
gymNotifier.setUseCountdownBetweenSets(newValue);
}
}
: null,
items: [false, true].map<DropdownMenuItem<bool>>((bool value) {
final label = value ? i18n.countdown : i18n.stopwatch;
return DropdownMenuItem<bool>(value: value, child: Text(label));
}).toList(),
),
subtitle: Text(i18n.gymModeTimerTypeHelText),
),
ListTile(
enabled: gymState.showTimerPages,
title: TextFormField(
controller: _countdownController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: i18n.gymModeDefaultCountdownTime,
suffix: IconButton(
onPressed: gymState.showTimerPages && gymState.useCountdownBetweenSets
? () => gymNotifier.setDefaultCountdownDuration(
DEFAULT_COUNTDOWN_DURATION,
)
: null,
icon: const Icon(Icons.refresh),
),
),
onChanged: (value) {
final intValue = int.tryParse(value);
if (intValue != null &&
intValue > 0 &&
intValue < MAX_COUNTDOWN_DURATION) {
gymNotifier.setDefaultCountdownDuration(intValue);
}
},
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (String? value) {
final intValue = int.tryParse(value!);
if (intValue == null ||
intValue < MIN_COUNTDOWN_DURATION ||
intValue > MAX_COUNTDOWN_DURATION) {
return i18n.formMinMaxValues(
MIN_COUNTDOWN_DURATION,
MAX_COUNTDOWN_DURATION,
);
}
return null;
},
enabled: gymState.showTimerPages && gymState.useCountdownBetweenSets,
),
// trailing: IconButton(
// onPressed: gymState.showTimerPages && gymState.useCountdownBetweenSets
// ? () => gymNotifier.setDefaultCountdownDuration(
// DEFAULT_COUNTDOWN_DURATION,
// )
// : null,
// icon: const Icon(Icons.refresh),
// ),
),
SwitchListTile(
key: const ValueKey('gym-mode-notify-countdown'),
title: Text(i18n.gymModeNotifyOnCountdownFinish),
value: gymState.alertOnCountdownEnd,
onChanged: (gymState.showTimerPages && gymState.useCountdownBetweenSets)
? (value) => gymNotifier.setAlertOnCountdownEnd(value)
: null,
),
],
),
),
SwitchListTile(
key: const ValueKey('gym-mode-option-show-timer'),
title: Text(i18n.gymModeShowTimer),
value: gymState.showTimerPages,
onChanged: (value) => gymNotifier.setShowTimerPages(value),
),
],
),
),
),
crossFadeState: _showOptions ? CrossFadeState.showSecond : CrossFadeState.showFirst,

View File

@@ -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
@@ -19,8 +19,10 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/gym_state.dart';
import 'package:wger/theme/theme.dart';
import 'package:wger/widgets/routines/gym_mode/navigation.dart';
@@ -71,8 +73,7 @@ class _TimerWidgetState extends State<TimerWidget> {
child: Center(
child: Text(
DateFormat('m:ss').format(displayTime),
style:
Theme.of(context).textTheme.displayLarge!.copyWith(color: wgerPrimaryColor),
style: Theme.of(context).textTheme.displayLarge!.copyWith(color: wgerPrimaryColor),
),
),
),
@@ -82,7 +83,7 @@ class _TimerWidgetState extends State<TimerWidget> {
}
}
class TimerCountdownWidget extends StatefulWidget {
class TimerCountdownWidget extends ConsumerStatefulWidget {
final PageController _controller;
final int _seconds;
@@ -95,13 +96,10 @@ class TimerCountdownWidget extends StatefulWidget {
_TimerCountdownWidgetState createState() => _TimerCountdownWidgetState();
}
class _TimerCountdownWidgetState extends State<TimerCountdownWidget> {
class _TimerCountdownWidgetState extends ConsumerState<TimerCountdownWidget> {
late DateTime _endTime;
late Timer _uiTimer;
// NEW: settings + one-time notification flag
bool _soundEnabled = true;
bool _vibrationEnabled = true;
bool _hasNotified = false;
@override
@@ -125,18 +123,18 @@ class _TimerCountdownWidgetState extends State<TimerCountdownWidget> {
Widget build(BuildContext context) {
final remaining = _endTime.difference(DateTime.now());
final remainingSeconds = remaining.inSeconds <= 0 ? 0 : remaining.inSeconds;
final displayTime =
DateTime(2000, 1, 1, 0, 0, 0).add(Duration(seconds: remainingSeconds));
final displayTime = DateTime(2000, 1, 1, 0, 0, 0).add(Duration(seconds: remainingSeconds));
final gymState = ref.watch(gymStateProvider);
// When countdown finishes, notify ONCE, and respect settings
if (remainingSeconds == 0 && !_hasNotified) {
if (_soundEnabled) {
if (gymState.alertOnCountdownEnd) {
SystemSound.play(SystemSoundType.alert);
}
if (_vibrationEnabled) {
HapticFeedback.mediumImpact();
}
_hasNotified = true;
setState(() {
_hasNotified = true;
});
}
return Column(
@@ -149,52 +147,11 @@ class _TimerCountdownWidgetState extends State<TimerCountdownWidget> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// countdown time
Text(
DateFormat('m:ss').format(displayTime),
style: Theme.of(context)
.textTheme
.displayLarge!
.copyWith(color: wgerPrimaryColor),
style: Theme.of(context).textTheme.displayLarge!.copyWith(color: wgerPrimaryColor),
),
const SizedBox(height: 16),
// NEW: simple settings row (Sound / Vibration)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Switch(
value: _soundEnabled,
onChanged: (value) {
setState(() {
_soundEnabled = value;
});
},
),
const SizedBox(width: 4),
const Text('Sound'),
],
),
const SizedBox(width: 24),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Switch(
value: _vibrationEnabled,
onChanged: (value) {
setState(() {
_vibrationEnabled = value;
});
},
),
const SizedBox(width: 4),
const Text('Vibration'),
],
),
],
),
],
),
),