diff --git a/lib/models/workouts/log.dart b/lib/models/workouts/log.dart index f4473aa1..52b5c9f6 100644 --- a/lib/models/workouts/log.dart +++ b/lib/models/workouts/log.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020 - 2025 wger Team + * Copyright (c) 2020 - 2026 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 @@ -101,13 +101,13 @@ class Log { this.repetitions, this.repetitionsTarget, this.repetitionsUnitId = REP_UNIT_REPETITIONS_ID, - required this.rir, + this.rir, this.rirTarget, this.weight, this.weightTarget, this.weightUnitId = WEIGHT_UNIT_KG, - required this.date, - }); + DateTime? date, + }) : date = date ?? DateTime.now(); Log.empty(); @@ -130,6 +130,51 @@ class Log { rirTarget = setConfig.rir; } + Log copyWith({ + int? id, + int? exerciseId, + int? routineId, + int? sessionId, + int? iteration, + int? slotEntryId, + num? rir, + num? rirTarget, + num? repetitions, + num? repetitionsTarget, + int? repetitionsUnitId, + num? weight, + num? weightTarget, + int? weightUnitId, + DateTime? date, + }) { + final out = Log( + id: id ?? this.id, + exerciseId: exerciseId ?? this.exerciseId, + iteration: iteration ?? this.iteration, + slotEntryId: slotEntryId ?? this.slotEntryId, + routineId: routineId ?? this.routineId, + repetitions: repetitions ?? this.repetitions, + repetitionsTarget: repetitionsTarget ?? this.repetitionsTarget, + repetitionsUnitId: repetitionsUnitId ?? this.repetitionsUnitId, + rir: rir ?? this.rir, + rirTarget: rirTarget ?? this.rirTarget, + weight: weight ?? this.weight, + weightTarget: weightTarget ?? this.weightTarget, + weightUnitId: weightUnitId ?? this.weightUnitId, + date: date ?? this.date, + ); + + if (sessionId != null) { + out.sessionId = sessionId; + } + + out.exerciseBase = exercise; + out.repetitionUnit = repetitionsUnitObj; + out.weightUnitObj = weightUnitObj; + + return out; + } + // Boilerplate factory Log.fromJson(Map json) => _$LogFromJson(json); diff --git a/lib/models/workouts/session.g.dart b/lib/models/workouts/session.g.dart index ac78029d..bf061fa1 100644 --- a/lib/models/workouts/session.g.dart +++ b/lib/models/workouts/session.g.dart @@ -23,7 +23,9 @@ WorkoutSession _$WorkoutSessionFromJson(Map json) { id: (json['id'] as num?)?.toInt(), dayId: (json['day'] as num?)?.toInt(), routineId: (json['routine'] as num?)?.toInt(), - impression: json['impression'] == null ? 2 : int.parse(json['impression'] as String), + impression: json['impression'] == null + ? DEFAULT_IMPRESSION + : int.parse(json['impression'] as String), notes: json['notes'] as String? ?? '', timeStart: stringToTimeNull(json['time_start'] as String?), timeEnd: stringToTimeNull(json['time_end'] as String?), diff --git a/lib/models/workouts/slot_data.g.dart b/lib/models/workouts/slot_data.g.dart index 589b2b99..756717e6 100644 --- a/lib/models/workouts/slot_data.g.dart +++ b/lib/models/workouts/slot_data.g.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (c) 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. - * - * This program 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 . - */ - // GENERATED CODE - DO NOT MODIFY BY HAND part of 'slot_data.dart'; diff --git a/lib/providers/gym_log_state.dart b/lib/providers/gym_log_state.dart new file mode 100644 index 00000000..96604c5b --- /dev/null +++ b/lib/providers/gym_log_state.dart @@ -0,0 +1,46 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 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. + * + * This program 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 . + */ + +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:wger/models/workouts/log.dart'; + +part 'gym_log_state.g.dart'; + +@Riverpod(keepAlive: true) +class GymLogNotifier extends _$GymLogNotifier { + final _logger = Logger('GymLogNotifier'); + + @override + Log? build() { + _logger.finer('Initializing GymLogNotifier'); + return null; + } + + void setLog(Log newLog) { + state = newLog; + } + + void setWeight(num weight) { + state = state?.copyWith(weight: weight); + } + + void setRepetitions(num repetitions) { + state = state?.copyWith(repetitions: repetitions); + } +} diff --git a/lib/providers/gym_log_state.g.dart b/lib/providers/gym_log_state.g.dart new file mode 100644 index 00000000..83c92cc1 --- /dev/null +++ b/lib/providers/gym_log_state.g.dart @@ -0,0 +1,56 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'gym_log_state.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(GymLogNotifier) +const gymLogProvider = GymLogNotifierProvider._(); + +final class GymLogNotifierProvider extends $NotifierProvider { + const GymLogNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'gymLogProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$gymLogNotifierHash(); + + @$internal + @override + GymLogNotifier create() => GymLogNotifier(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Log? value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$gymLogNotifierHash() => r'f7cdc8f72506e366ca028360b654da0bdd9bcae6'; + +abstract class _$GymLogNotifier extends $Notifier { + Log? build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element as $ClassProviderElement, Log?, Object?, Object?>; + element.handleValue(ref, created); + } +} diff --git a/lib/providers/gym_state.dart b/lib/providers/gym_state.dart index 949a753f..882df366 100644 --- a/lib/providers/gym_state.dart +++ b/lib/providers/gym_state.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2025 wger Team + * Copyright (c) 2026 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 @@ -25,8 +25,10 @@ import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/helpers/uuid.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'; +import 'package:wger/providers/gym_log_state.dart'; part 'gym_state.g.dart'; @@ -131,7 +133,11 @@ class SlotPageEntry { this.setConfigData, this.logDone = false, String? uuid, - }) : uuid = uuid ?? uuidV4(); + }) : assert( + type != SlotPageType.log || setConfigData != null, + 'You need to set setConfigData for SlotPageType.log', + ), + uuid = uuid ?? uuidV4(); SlotPageEntry copyWith({ String? uuid, @@ -481,7 +487,7 @@ class GymStateNotifier extends _$GymStateNotifier { pages.add(PageEntry(type: PageType.workoutSummary, pageIndex: pageIndex + 1)); state = state.copyWith(pages: pages); - print(readPageStructure()); + // print(readPageStructure()); _logger.finer('Initialized ${state.pages.length} pages'); } @@ -573,6 +579,17 @@ class GymStateNotifier extends _$GymStateNotifier { void setCurrentPage(int page) { state = state.copyWith(currentPage: page); + + // Ensure that there is a log entry for the current slot entry + final slotEntryPage = state.getSlotEntryPageByIndex(); + if (slotEntryPage == null || slotEntryPage.setConfigData == null) { + return; + } + + final log = Log.fromSetConfigData(slotEntryPage.setConfigData!); + log.routineId = state.routine.id!; + log.iteration = state.iteration; + ref.read(gymLogProvider.notifier).setLog(log); } void setShowExercisePages(bool value) { diff --git a/lib/providers/gym_state.g.dart b/lib/providers/gym_state.g.dart index 7596fa4b..1899e808 100644 --- a/lib/providers/gym_state.g.dart +++ b/lib/providers/gym_state.g.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (c) 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. - * - * This program 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 . - */ - // GENERATED CODE - DO NOT MODIFY BY HAND part of 'gym_state.dart'; diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index 70254266..088836d6 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020 - 2025 wger Team + * Copyright (c) 2020 - 2026 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 @@ -27,6 +27,7 @@ import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/log.dart'; import 'package:wger/models/workouts/set_config_data.dart'; import 'package:wger/models/workouts/slot_entry.dart'; +import 'package:wger/providers/gym_log_state.dart'; import 'package:wger/providers/gym_state.dart'; import 'package:wger/providers/plate_weights.dart'; import 'package:wger/providers/routines.dart'; @@ -39,75 +40,37 @@ import 'package:wger/widgets/routines/forms/weight_unit.dart'; import 'package:wger/widgets/routines/gym_mode/navigation.dart'; import 'package:wger/widgets/routines/plate_calculator.dart'; -class LogPage extends ConsumerStatefulWidget { +class LogPage extends ConsumerWidget { final _logger = Logger('LogPage'); final PageController _controller; - LogPage(this._controller); - - @override - _LogPageState createState() => _LogPageState(); -} - -class _LogPageState extends ConsumerState { final GlobalKey<_LogFormWidgetState> _logFormKey = GlobalKey<_LogFormWidgetState>(); - late FocusNode focusNode; - // Persistent log and current slot-page id to avoid recreating the Log on rebuilds - Log? _currentLog; - String? _currentSlotPageUuid; - @override - void initState() { - super.initState(); - focusNode = FocusNode(); - } - - @override - void dispose() { - focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); - final state = ref.watch(gymStateProvider); + final gymState = ref.watch(gymStateProvider); final languageCode = Localizations.localeOf(context).languageCode; - final page = state.getPageByIndex(); + final page = gymState.getPageByIndex(); if (page == null) { - widget._logger.info( - 'getPageByIndex for ${state.currentPage} returned null, showing empty container.', + _logger.info( + 'getPageByIndex for ${gymState.currentPage} returned null, showing empty container.', ); return Container(); } - final slotEntryPage = state.getSlotEntryPageByIndex(); + final slotEntryPage = gymState.getSlotEntryPageByIndex(); if (slotEntryPage == null) { - widget._logger.info( - 'getSlotPageByIndex for ${state.currentPage} returned null, showing empty container', + _logger.info( + 'getSlotPageByIndex for ${gymState.currentPage} returned null, showing empty container', ); return Container(); } - final setConfigData = slotEntryPage.setConfigData!; - // Create a Log only when the slot page changed or none exists yet - if (_currentLog == null || _currentSlotPageUuid != slotEntryPage.uuid) { - _currentLog = Log.fromSetConfigData(setConfigData) - ..routineId = state.routine.id! - ..iteration = state.iteration; - _currentSlotPageUuid = slotEntryPage.uuid; - } else { - // Update routine/iteration if needed without creating a new Log - _currentLog! - ..routineId = state.routine.id! - ..iteration = state.iteration; - } - - final log = _currentLog!; + final log = ref.watch(gymLogProvider); // Mark done sets final decorationStyle = slotEntryPage.logDone @@ -117,8 +80,9 @@ class _LogPageState extends ConsumerState { return Column( children: [ NavigationHeader( - log.exercise.getTranslation(languageCode).name, - widget._controller, + log!.exercise.getTranslation(languageCode).name, + _controller, + key: const ValueKey('log-page-navigation-header'), ), Container( @@ -164,16 +128,9 @@ class _LogPageState extends ConsumerState { Text(slotEntryPage.setConfigData!.comment, textAlign: TextAlign.center), const SizedBox(height: 10), Expanded( - child: (state.routine.filterLogsByExercise(log.exercise.id!).isNotEmpty) + child: (gymState.routine.filterLogsByExercise(log.exerciseId).isNotEmpty) ? LogsPastLogsWidget( - log: log, - pastLogs: state.routine.filterLogsByExercise(log.exercise.id!), - onCopy: (pastLog) { - _logFormKey.currentState?.copyFromPastLog(pastLog); - }, - setStateCallback: (fn) { - setState(fn); - }, + pastLogs: gymState.routine.filterLogsByExercise(log.exerciseId), ) : Container(), ), @@ -186,16 +143,15 @@ class _LogPageState extends ConsumerState { child: Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: LogFormWidget( - controller: widget._controller, + controller: _controller, configData: setConfigData, - log: log, - focusNode: focusNode, + // log: log!, key: _logFormKey, ), ), ), ), - NavigationFooter(widget._controller), + NavigationFooter(_controller), ], ); } @@ -255,68 +211,62 @@ class LogsPlatesWidget extends ConsumerWidget { } } -class LogsRepsWidget extends StatelessWidget { - final TextEditingController controller; - final SetConfigData configData; - final FocusNode focusNode; - final Log log; - final void Function(VoidCallback fn) setStateCallback; - +class LogsRepsWidget extends ConsumerWidget { final _logger = Logger('LogsRepsWidget'); + final num valueChange; + LogsRepsWidget({ super.key, - required this.controller, - required this.configData, - required this.focusNode, - required this.log, - required this.setStateCallback, - }); + num? valueChange, + }) : valueChange = valueChange ?? 1; @override - Widget build(BuildContext context) { - final repsValueChange = configData.repetitionsRounding ?? 1; + Widget build(BuildContext context, WidgetRef ref) { final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString()); - final i18n = AppLocalizations.of(context); + final logNotifier = ref.read(gymLogProvider.notifier); + final log = ref.watch(gymLogProvider); + + final currentReps = log?.repetitions; + final repText = currentReps != null ? numberFormat.format(currentReps) : ''; + return Row( children: [ + // "Quick-remove" button IconButton( icon: const Icon(Icons.remove, color: Colors.black), onPressed: () { - final currentValue = numberFormat.tryParse(controller.text) ?? 0; - final newValue = currentValue - repsValueChange; - if (newValue >= 0) { - setStateCallback(() { - log.repetitions = newValue; - controller.text = numberFormat.format(newValue); - }); + final base = currentReps ?? 0; + final newValue = base - valueChange; + if (newValue >= 0 && log != null) { + logNotifier.setRepetitions(newValue); } }, ), + + // Text field Expanded( child: TextFormField( decoration: InputDecoration(labelText: i18n.repetitions), enabled: true, - controller: controller, + key: ValueKey('reps-field-$repText'), + initialValue: repText, keyboardType: textInputTypeDecimal, - focusNode: focusNode, onChanged: (value) { try { final newValue = numberFormat.parse(value); - setStateCallback(() { - log.repetitions = newValue; - }); + logNotifier.setRepetitions(newValue); } on FormatException catch (error) { - _logger.fine('Error parsing repetitions: $error'); + _logger.finer('Error parsing repetitions: $error'); } }, onSaved: (newValue) { - _logger.info('Saving new reps value: $newValue'); - setStateCallback(() { - log.repetitions = numberFormat.parse(newValue!); - }); + if (newValue == null || log == null) { + return; + } + logNotifier.setRepetitions(numberFormat.parse(newValue)); }, validator: (value) { if (numberFormat.tryParse(value ?? '') == null) { @@ -326,19 +276,15 @@ class LogsRepsWidget extends StatelessWidget { }, ), ), + + // "Quick-add" button IconButton( icon: const Icon(Icons.add, color: Colors.black), onPressed: () { - final value = controller.text.isNotEmpty ? controller.text : '0'; - - try { - final newValue = numberFormat.parse(value) + repsValueChange; - setStateCallback(() { - log.repetitions = newValue; - controller.text = numberFormat.format(newValue); - }); - } on FormatException catch (error) { - _logger.fine('Error parsing reps during quick-add: $error'); + final base = currentReps ?? 0; + final newValue = base + valueChange; + if (newValue >= 0 && log != null) { + logNotifier.setRepetitions(newValue); } }, ), @@ -348,76 +294,62 @@ class LogsRepsWidget extends StatelessWidget { } class LogsWeightWidget extends ConsumerWidget { - final TextEditingController controller; - final SetConfigData configData; - final FocusNode focusNode; - final Log log; - final void Function(VoidCallback fn) setStateCallback; - final _logger = Logger('LogsWeightWidget'); + final num valueChange; + LogsWeightWidget({ super.key, - required this.controller, - required this.configData, - required this.focusNode, - required this.log, - required this.setStateCallback, - }); + num? valueChange, + }) : valueChange = valueChange ?? 1.25; @override Widget build(BuildContext context, WidgetRef ref) { - final weightValueChange = configData.weightRounding ?? 1.25; final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString()); final i18n = AppLocalizations.of(context); + final plateProvider = ref.read(plateCalculatorProvider.notifier); + final logProvider = ref.read(gymLogProvider.notifier); + final log = ref.watch(gymLogProvider); + + final currentWeight = log?.weight; + final weightText = currentWeight != null ? numberFormat.format(currentWeight) : ''; + return Row( children: [ IconButton( + // "Quick-remove" button icon: const Icon(Icons.remove, color: Colors.black), onPressed: () { - try { - final newValue = numberFormat.parse(controller.text) - weightValueChange; - if (newValue > 0) { - setStateCallback(() { - log.weight = newValue; - controller.text = numberFormat.format(newValue); - ref - .read(plateCalculatorProvider.notifier) - .setWeight( - controller.text == '' ? 0 : newValue, - ); - }); - } - } on FormatException catch (error) { - _logger.fine('Error parsing weight during quick-remove: $error'); + final base = currentWeight ?? 0; + final newValue = base - valueChange; + if (newValue >= 0 && log != null) { + logProvider.setWeight(newValue); } }, ), + + // Text field Expanded( child: TextFormField( + key: ValueKey('weight-field-$weightText'), decoration: InputDecoration(labelText: i18n.weight), - controller: controller, + initialValue: weightText, keyboardType: textInputTypeDecimal, onChanged: (value) { try { final newValue = numberFormat.parse(value); - setStateCallback(() { - log.weight = newValue; - ref - .read(plateCalculatorProvider.notifier) - .setWeight( - controller.text == '' ? 0 : numberFormat.parse(controller.text), - ); - }); + plateProvider.setWeight(newValue); + logProvider.setWeight(newValue); } on FormatException catch (error) { - _logger.fine('Error parsing weight: $error'); + _logger.finer('Error parsing weight: $error'); } }, onSaved: (newValue) { - setStateCallback(() { - log.weight = numberFormat.parse(newValue!); - }); + if (newValue == null || log == null) { + return; + } + logProvider.setWeight(numberFormat.parse(newValue)); }, validator: (value) { if (numberFormat.tryParse(value ?? '') == null) { @@ -427,24 +359,15 @@ class LogsWeightWidget extends ConsumerWidget { }, ), ), + + // "Quick-add" button IconButton( icon: const Icon(Icons.add, color: Colors.black), onPressed: () { - final value = controller.text.isNotEmpty ? controller.text : '0'; - - try { - final newValue = numberFormat.parse(value) + weightValueChange; - setStateCallback(() { - log.weight = newValue; - controller.text = numberFormat.format(newValue); - ref - .read(plateCalculatorProvider.notifier) - .setWeight( - controller.text == '' ? 0 : newValue, - ); - }); - } on FormatException catch (error) { - _logger.fine('Error parsing weight during quick-add: $error'); + final base = currentWeight ?? 0; + final newValue = base + valueChange; + if (log != null) { + logProvider.setWeight(newValue); } }, ), @@ -453,22 +376,19 @@ class LogsWeightWidget extends ConsumerWidget { } } -class LogsPastLogsWidget extends StatelessWidget { - final Log log; +class LogsPastLogsWidget extends ConsumerWidget { final List pastLogs; - final void Function(Log pastLog) onCopy; - final void Function(VoidCallback fn) setStateCallback; const LogsPastLogsWidget({ super.key, - required this.log, required this.pastLogs, - required this.onCopy, - required this.setStateCallback, }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final logProvider = ref.read(gymLogProvider.notifier); + final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode); + return Container( padding: const EdgeInsets.symmetric(vertical: 8), child: ListView( @@ -482,25 +402,16 @@ class LogsPastLogsWidget extends StatelessWidget { return ListTile( key: ValueKey('past-log-${pastLog.id}'), title: Text(pastLog.repTextNoNl(context)), - subtitle: Text( - DateFormat.yMd(Localizations.localeOf(context).languageCode).format(pastLog.date), - ), + subtitle: Text(dateFormat.format(pastLog.date)), trailing: const Icon(Icons.copy), onTap: () { - setStateCallback(() { - log.rir = pastLog.rir; - log.repetitionUnit = pastLog.repetitionsUnitObj; - log.weightUnit = pastLog.weightUnitObj; - - onCopy(pastLog); - - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(AppLocalizations.of(context).dataCopied), - ), - ); - }); + logProvider.setLog(pastLog); + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context).dataCopied), + ), + ); }, contentPadding: const EdgeInsets.symmetric(horizontal: 40), ); @@ -516,15 +427,11 @@ class LogFormWidget extends ConsumerStatefulWidget { final PageController controller; final SetConfigData configData; - final Log log; - final FocusNode focusNode; LogFormWidget({ super.key, required this.controller, required this.configData, - required this.log, - required this.focusNode, }); @override @@ -535,116 +442,11 @@ class _LogFormWidgetState extends ConsumerState { final _form = GlobalKey(); var _detailed = false; bool _isSaving = false; - late Log _log; - - late final TextEditingController _repetitionsController; - late final TextEditingController _weightController; - - @override - void initState() { - super.initState(); - - _log = widget.log; - _repetitionsController = TextEditingController(); - _weightController = TextEditingController(); - - WidgetsBinding.instance.addPostFrameCallback((_) { - _syncControllersWithWidget(); - }); - } - - @override - void didUpdateWidget(covariant LogFormWidget oldWidget) { - super.didUpdateWidget(oldWidget); - - // If the log or config changed, update internal _log and controllers - if (oldWidget.log != widget.log || oldWidget.configData != widget.configData) { - _log = widget.log; - _syncControllersWithWidget(); - } - } - - void _syncControllersWithWidget() { - final locale = Localizations.localeOf(context).toString(); - final numberFormat = NumberFormat.decimalPattern(locale); - - // Priority: current log -> config defaults -> empty - try { - _repetitionsController.text = widget.log.repetitions != null - ? numberFormat.format(widget.log.repetitions) - : (widget.configData.repetitions != null - ? numberFormat.format(widget.configData.repetitions) - : ''); - - _weightController.text = widget.log.weight != null - ? numberFormat.format(widget.log.weight) - : (widget.configData.weight != null ? numberFormat.format(widget.configData.weight) : ''); - } on Exception catch (e) { - // Defensive fallback: set empty strings if formatting fails - widget._logger.warning('Error syncing controllers: $e'); - _repetitionsController.text = ''; - _weightController.text = ''; - } - } - - @override - void dispose() { - _repetitionsController.dispose(); - _weightController.dispose(); - super.dispose(); - } - - void copyFromPastLog(Log pastLog) { - final locale = Localizations.localeOf(context).toString(); - final numberFormat = NumberFormat.decimalPattern(locale); - - setState(() { - _repetitionsController.text = pastLog.repetitions != null - ? numberFormat.format(pastLog.repetitions) - : ''; - widget._logger.finer('Setting log repetitions to ${_repetitionsController.text}'); - - _weightController.text = pastLog.weight != null ? numberFormat.format(pastLog.weight) : ''; - widget._logger.finer('Setting log weight to ${_weightController.text}'); - - _log.repetitions = pastLog.repetitions; - _log.weight = pastLog.weight; - _log.rir = pastLog.rir; - if (pastLog.repetitionsUnitObj != null) { - _log.repetitionUnit = pastLog.repetitionsUnitObj; - } - if (pastLog.weightUnitObj != null) { - _log.weightUnit = pastLog.weightUnitObj; - } - - widget._logger.finer( - 'Copied to _log: repetitions=${_log.repetitions}, weight=${_log.weight}, repetitionsUnitId=${_log.repetitionsUnitId}, weightUnitId=${_log.weightUnitId}, rir=${_log.rir}', - ); - - // Update plate calculator using the value currently visible in the controllers - try { - final weightValue = _weightController.text.isEmpty - ? 0 - : numberFormat.parse(_weightController.text); - ref.read(plateCalculatorProvider.notifier).setWeight(weightValue); - } catch (e) { - widget._logger.fine('Error updating plate calculator: $e'); - } - }); - - // Ensure subsequent syncs (e.g., didUpdateWidget) don't overwrite these values - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!mounted) { - return; - } - - _syncControllersWithWidget(); - }); - } @override Widget build(BuildContext context) { final i18n = AppLocalizations.of(context); + final log = ref.watch(gymLogProvider); return Form( key: _form, @@ -662,26 +464,14 @@ class _LogFormWidgetState extends ConsumerState { Flexible( child: LogsRepsWidget( key: const ValueKey('logs-reps-widget'), - controller: _repetitionsController, - configData: widget.configData, - focusNode: widget.focusNode, - log: _log, - setStateCallback: (fn) { - setState(fn); - }, + valueChange: widget.configData.repetitionsRounding, ), ), const SizedBox(width: 8), Flexible( child: LogsWeightWidget( key: const ValueKey('logs-weight-widget'), - controller: _weightController, - configData: widget.configData, - focusNode: widget.focusNode, - log: _log, - setStateCallback: (fn) { - setState(fn); - }, + valueChange: widget.configData.weightRounding, ), ), ], @@ -693,20 +483,14 @@ class _LogFormWidgetState extends ConsumerState { Flexible( child: LogsRepsWidget( key: const ValueKey('logs-reps-widget'), - controller: _repetitionsController, - configData: widget.configData, - focusNode: widget.focusNode, - log: _log, - setStateCallback: (fn) { - setState(fn); - }, + valueChange: widget.configData.repetitionsRounding, ), ), const SizedBox(width: 8), Flexible( child: RepetitionUnitInputWidget( key: const ValueKey('repetition-unit-input-widget'), - _log.repetitionsUnitId, + log!.repetitionsUnitId, onChanged: (v) => {}, ), ), @@ -720,19 +504,13 @@ class _LogFormWidgetState extends ConsumerState { Flexible( child: LogsWeightWidget( key: const ValueKey('logs-weight-widget'), - controller: _weightController, - configData: widget.configData, - focusNode: widget.focusNode, - log: _log, - setStateCallback: (fn) { - setState(fn); - }, + valueChange: widget.configData.weightRounding, ), ), const SizedBox(width: 8), Flexible( child: WeightUnitInputWidget( - _log.weightUnitId, + log!.weightUnitId, onChanged: (v) => {}, key: const ValueKey('weight-unit-input-widget'), ), @@ -743,13 +521,9 @@ class _LogFormWidgetState extends ConsumerState { if (_detailed) RiRInputWidget( key: const ValueKey('rir-input-widget'), - _log.rir, + log!.rir, onChanged: (value) { - if (value == '') { - _log.rir = null; - } else { - _log.rir = num.parse(value); - } + log.rir = value == '' ? null : num.parse(value); }, ), SwitchListTile( @@ -782,7 +556,7 @@ class _LogFormWidgetState extends ConsumerState { await provider.Provider.of( context, listen: false, - ).addLog(_log); + ).addLog(log!); final page = gymState.getSlotEntryPageByIndex()!; gymProvider.markSlotPageAsDone(page.uuid, isDone: true); diff --git a/lib/widgets/routines/gym_mode/navigation.dart b/lib/widgets/routines/gym_mode/navigation.dart index d2572b52..9639c3d0 100644 --- a/lib/widgets/routines/gym_mode/navigation.dart +++ b/lib/widgets/routines/gym_mode/navigation.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 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, + * This program 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. @@ -28,7 +28,12 @@ class NavigationHeader extends StatelessWidget { final String _title; final bool showEndWorkoutButton; - const NavigationHeader(this._title, this._controller, {this.showEndWorkoutButton = true}); + const NavigationHeader( + this._title, + this._controller, { + this.showEndWorkoutButton = true, + super.key, + }); @override Widget build(BuildContext context) { diff --git a/test/core/validators_test.mocks.dart b/test/core/validators_test.mocks.dart index 8791d455..9b069055 100644 --- a/test/core/validators_test.mocks.dart +++ b/test/core/validators_test.mocks.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (c) 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. - * - * This program 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 . - */ - // Mocks generated by Mockito 5.4.6 from annotations // in wger/test/core/validators_test.dart. // Do not manually edit this file. diff --git a/test/routine/gym_mode/gym_mode_test.dart b/test/routine/gym_mode/gym_mode_test.dart index 38a3e296..3f61e88b 100644 --- a/test/routine/gym_mode/gym_mode_test.dart +++ b/test/routine/gym_mode/gym_mode_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020, 2025 wger Team + * Copyright (c) 2020 - 2026 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 @@ -109,12 +109,10 @@ void main() { await withClock(Clock.fixed(DateTime(2025, 3, 29, 14, 33)), () async { await tester.pumpWidget(renderGymMode()); - await tester.pumpAndSettle(); await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); - //await tester.ensureVisible(find.byKey(Key(key as String))); + // // Start page // @@ -306,6 +304,7 @@ void main() { expect(find.byIcon(Icons.chevron_right), findsNothing); }); }, + tags: ['golden'], semanticsEnabled: false, ); } diff --git a/test/user/provider_test.mocks.dart b/test/user/provider_test.mocks.dart index b8fce543..08bbf636 100644 --- a/test/user/provider_test.mocks.dart +++ b/test/user/provider_test.mocks.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (c) 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. - * - * This program 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 . - */ - // Mocks generated by Mockito 5.4.6 from annotations // in wger/test/user/provider_test.dart. // Do not manually edit this file. diff --git a/test/widgets/routines/gym_mode/log_page_test.dart b/test/widgets/routines/gym_mode/log_page_test.dart index 45526e7f..b9ba0f8d 100644 --- a/test/widgets/routines/gym_mode/log_page_test.dart +++ b/test/widgets/routines/gym_mode/log_page_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2025 - 2025 wger Team + * Copyright (c) 2025 - 2026 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 @@ -128,6 +128,7 @@ void main() { // Act notifier.calculatePages(); + notifier.setCurrentPage(2); // Assert expect(notifier.state.getSlotEntryPageByIndex()!.type, SlotPageType.log); @@ -159,6 +160,7 @@ void main() { iteration: 1, ); notifier.calculatePages(); + notifier.setCurrentPage(2); // Act // Log page is at index 2 @@ -197,6 +199,7 @@ void main() { iteration: 1, ); notifier.calculatePages(); + notifier.setCurrentPage(2); notifier.state = notifier.state.copyWith(currentPage: 2); final mockRoutines = MockRoutinesProvider(); @@ -206,8 +209,8 @@ void main() { final editableFields = find.byType(EditableText); expect(editableFields, findsWidgets); - await tester.enterText(editableFields.at(0), '7'); - await tester.enterText(editableFields.at(1), '77'); + await tester.enterText(editableFields.at(0), '12'); // Reps + await tester.enterText(editableFields.at(1), '34'); // Weight await tester.pumpAndSettle(); Log? capturedLog; @@ -226,8 +229,8 @@ void main() { // Assert verify(mockRoutines.addLog(any)).called(1); expect(capturedLog, isNotNull); - expect(capturedLog!.repetitions, equals(7)); - expect(capturedLog!.weight, equals(77)); + expect(capturedLog!.repetitions, equals(12)); + expect(capturedLog!.weight, equals(34)); final currentSlotPage = notifier.state.getSlotEntryPageByIndex()!; expect(capturedLog!.slotEntryId, equals(currentSlotPage.setConfigData!.slotEntryId)); diff --git a/test/widgets/routines/gym_mode/log_page_test.mocks.dart b/test/widgets/routines/gym_mode/log_page_test.mocks.dart index 500c61d8..e9105c48 100644 --- a/test/widgets/routines/gym_mode/log_page_test.mocks.dart +++ b/test/widgets/routines/gym_mode/log_page_test.mocks.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (c) 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. - * - * This program 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 . - */ - // Mocks generated by Mockito 5.4.6 from annotations // in wger/test/widgets/routines/gym_mode/log_page_test.dart. // Do not manually edit this file.