Simplify code by adding new log provider

This makes the logic for copying or modifying the logs much easier. Also,
there were some user reports that the old logic sometimes behaved erratically
and old values were sometimes reverted.
This commit is contained in:
Roland Geider
2026-01-12 21:39:36 +01:00
parent 994c962921
commit fb6a673503
14 changed files with 302 additions and 445 deletions

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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<String, dynamic> json) => _$LogFromJson(json);

View File

@@ -23,7 +23,9 @@ WorkoutSession _$WorkoutSessionFromJson(Map<String, dynamic> 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?),

View File

@@ -1,21 +1,3 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'slot_data.dart';

View File

@@ -0,0 +1,46 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -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<GymLogNotifier, Log?> {
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<Log?>(value),
);
}
}
String _$gymLogNotifierHash() => r'f7cdc8f72506e366ca028360b654da0bdd9bcae6';
abstract class _$GymLogNotifier extends $Notifier<Log?> {
Log? build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<Log?, Log?>;
final element =
ref.element as $ClassProviderElement<AnyNotifier<Log?, Log?>, Log?, Object?, Object?>;
element.handleValue(ref, created);
}
}

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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) {

View File

@@ -1,21 +1,3 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'gym_state.dart';

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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<LogPage> {
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<LogPage> {
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<LogPage> {
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<LogPage> {
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<Log> 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<LogFormWidget> {
final _form = GlobalKey<FormState>();
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<LogFormWidget> {
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<LogFormWidget> {
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<LogFormWidget> {
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<LogFormWidget> {
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<LogFormWidget> {
await provider.Provider.of<RoutinesProvider>(
context,
listen: false,
).addLog(_log);
).addLog(log!);
final page = gymState.getSlotEntryPageByIndex()!;
gymProvider.markSlotPageAsDone(page.uuid, isDone: true);

View File

@@ -1,13 +1,13 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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) {

View File

@@ -1,21 +1,3 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
// Mocks generated by Mockito 5.4.6 from annotations
// in wger/test/core/validators_test.dart.
// Do not manually edit this file.

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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,
);
}

View File

@@ -1,21 +1,3 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
// Mocks generated by Mockito 5.4.6 from annotations
// in wger/test/user/provider_test.dart.
// Do not manually edit this file.

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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));

View File

@@ -1,21 +1,3 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
// 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.