mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Allow some options for the user to control the timer between sets
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user