Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/core/network/network_provider.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 32 additions & 4 deletions lib/core/widgets/datetime_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,30 @@ class _TimeInputWidgetState extends State<TimeInputWidget> {
_value = widget.value;
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
// Format depends on MaterialLocalizations, so (re)sync once dependencies
// are available rather than during build (which would notify the parent
// Form mid-build and trigger setState-during-build).
_syncText();
}

@override
void didUpdateWidget(TimeInputWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value != oldWidget.value) {
_value = widget.value;
_syncText();
}
}

/// Rewrites the read-only display text to match [_value]. Must be called
/// outside of [build] because it notifies the controller's listeners.
void _syncText() {}

@override
Widget build(BuildContext context) {
// Keyed initialValue, not a controller: a controller notifies the enclosing
// Form on assignment, which crashes if that happens during a build.
return TextFormField(
key: ValueKey(_value),
readOnly: true,
Expand All @@ -91,6 +103,7 @@ class _TimeInputWidgetState extends State<TimeInputWidget> {
icon: const Icon(Icons.clear),
onPressed: () {
setState(() => _value = null);
_syncText();
widget.onCleared!();
},
)
Expand All @@ -106,6 +119,7 @@ class _TimeInputWidgetState extends State<TimeInputWidget> {
);
if (picked != null && context.mounted) {
setState(() => _value = picked);
_syncText();
widget.onChanged(picked);
}
},
Expand Down Expand Up @@ -171,19 +185,31 @@ class _DateInputWidgetState extends State<DateInputWidget> {
_value = widget.value;
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
// Format depends on the locale, so (re)sync once dependencies are
// available rather than during build (which would notify the parent Form
// mid-build and trigger setState-during-build).
_syncText();
}

@override
void didUpdateWidget(DateInputWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value != oldWidget.value) {
_value = widget.value;
_syncText();
}
}

/// Rewrites the read-only display text to match [_value]. Must be called
/// outside of [build] because it notifies the controller's listeners.
void _syncText() {}

@override
Widget build(BuildContext context) {
final dateFormat = localizedDate(context);
// Keyed initialValue, not a controller: a controller notifies the enclosing
// Form on assignment, which crashes if that happens during a build.
return TextFormField(
key: ValueKey(_value),
readOnly: true,
Expand All @@ -198,6 +224,7 @@ class _DateInputWidgetState extends State<DateInputWidget> {
icon: const Icon(Icons.clear),
onPressed: () {
setState(() => _value = null);
_syncText();
widget.onCleared!();
},
)
Expand All @@ -215,6 +242,7 @@ class _DateInputWidgetState extends State<DateInputWidget> {
);
if (picked != null && context.mounted) {
setState(() => _value = picked);
_syncText();
widget.onChanged(picked);
}
},
Expand Down
2 changes: 1 addition & 1 deletion lib/database/powersync/powersync.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 9 additions & 6 deletions lib/features/routines/models/log.dart
Original file line number Diff line number Diff line change
Expand Up @@ -241,18 +241,21 @@ class Log {
// is no weight defined so that we don't just output something like "8" but
// rather "8 repetitions". If there is weight we want to output "8 x 50kg",
// since the repetitions are implied. If other units are used, we always
// print them
if (repetitionsUnitObj != null && repetitionsUnitObj!.id != REP_UNIT_REPETITIONS_ID ||
weight == 0 ||
weight == null) {
out.add(getServerStringTranslation(repetitionsUnitObj!.name, context));
// print them. The unit object may be missing (not yet hydrated), in which
// case we simply omit the label rather than crash.
final repUnit = repetitionsUnitObj;
final isNonDefaultRepUnit = repUnit != null && repUnit.id != REP_UNIT_REPETITIONS_ID;
if ((isNonDefaultRepUnit || weight == 0 || weight == null) && repUnit != null) {
out.add(getServerStringTranslation(repUnit.name, context));
}
}

if (weight != null && weight != 0) {
out.add('×');
out.add(formatNum(weight!).toString());
out.add(weightUnitObj!.name);
if (weightUnitObj != null) {
out.add(weightUnitObj!.name);
}
}

if (rir != null) {
Expand Down
2 changes: 1 addition & 1 deletion lib/features/routines/providers/gym_log_notifier.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 44 additions & 1 deletion lib/features/routines/providers/gym_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/

import 'package:clock/clock.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:wger/core/uuid.dart';
import 'package:wger/features/exercises/models/exercise.dart';
Expand Down Expand Up @@ -91,7 +92,10 @@ class PageEntry {
List<Exercise> get exercises {
final exerciseSet = <Exercise>{};
for (final entry in slotPages) {
exerciseSet.add(entry.setConfigData!.exercise);
final exercise = entry.setConfigData?.exercise;
if (exercise != null) {
exerciseSet.add(exercise);
}
}
return exerciseSet.toList();
}
Expand Down Expand Up @@ -317,6 +321,45 @@ class GymModeState {
return null;
}

/// Maps a model [pageIndex] to its index within the gym-mode `PageView`.
///
/// The model assigns a [pageIndex] to every slot page (including
/// exercise-overview and rest-timer pages), but the `PageView` renders only
/// the start page, **one page per exercise** (set [PageEntry]), and the
/// session + summary pages. This translation keeps navigation (queue jumps,
/// auto-advance, finish) landing on the correct rendered page.
int renderIndexFor(int pageIndex) {
final setPages = pages.where((p) => p.type == PageType.set).toList();
final session = pages.firstWhereOrNull((p) => p.type == PageType.session);

for (var i = 0; i < setPages.length; i++) {
final start = setPages[i].pageIndex;
final end = (i + 1 < setPages.length)
? setPages[i + 1].pageIndex
: (session?.pageIndex ?? (1 << 30));
if (pageIndex >= start && pageIndex < end) {
return i + 1; // index 0 is the start page
}
}

// Past the last exercise: the session page comes first, then the summary.
if (session != null && pageIndex > session.pageIndex) {
return setPages.length + 2; // summary
}
return setPages.length + 1; // session
}

/// The set [PageEntry] rendered at PageView index [renderIndex], or null if
/// that index is the start, session or summary page (which have no exercise
/// queue / header chrome). See [renderIndexFor] for the index mapping.
PageEntry? setPageForRenderIndex(int renderIndex) {
final setPages = pages.where((p) => p.type == PageType.set).toList();
if (renderIndex >= 1 && renderIndex <= setPages.length) {
return setPages[renderIndex - 1];
}
return null;
}

SlotPageEntry? getSlotPageByUUID(String uuid) {
for (final slotPage in pages.expand((p) => p.slotPages)) {
if (slotPage.uuid == uuid) {
Expand Down
83 changes: 68 additions & 15 deletions lib/features/routines/providers/gym_state_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:wger/core/shared_preferences.dart';
import 'package:wger/features/exercises/models/exercise.dart';
import 'package:wger/features/routines/models/log.dart';
import 'package:wger/features/routines/models/routine.dart';
import 'package:wger/features/routines/models/set_config_data.dart';
import 'package:wger/features/routines/providers/gym_log_notifier.dart';
import 'package:wger/features/routines/providers/gym_state.dart';
import 'package:wger/features/routines/providers/rest_timer_notifier.dart';

part 'gym_state_notifier.g.dart';

Expand Down Expand Up @@ -303,19 +302,6 @@ 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!,
routineId: state.routine.id,
iteration: state.iteration,
);
ref.read(gymLogProvider.notifier).setLog(log);
}

void setShowExercisePages(bool value) {
Expand Down Expand Up @@ -468,6 +454,72 @@ class GymStateNotifier extends _$GymStateNotifier {
recalculateIndices();
}

void addSetToPage(String pageUUID) {
final updatedPages = state.pages.map((page) {
if (page.type != PageType.set || page.uuid != pageUUID) {
return page;
}
final logSlotPages = page.slotPages.where((sp) => sp.type == SlotPageType.log).toList();
final lastLog = logSlotPages.isNotEmpty ? logSlotPages.last : null;
// Seed the new set from the last logged set so it carries the exercise,
// target and comment. When the page has no logged set yet (e.g. only an
// exercise-overview slot exists) fall back to any sibling slot's config so
// the new slot never ends up with a null setConfigData — a log SlotPageEntry
// requires one, and a null would crash the page's exercise lookup.
final seedConfig =
lastLog?.setConfigData ??
page.slotPages.firstWhereOrNull((sp) => sp.setConfigData != null)?.setConfigData;
if (seedConfig == null) {
_logger.warning('Cannot add a set to page $pageUUID: no set config to seed from');
return page;
}
final newSlotPages = [...page.slotPages];
newSlotPages.add(
SlotPageEntry(
type: SlotPageType.log,
pageIndex: 0,
setIndex: page.slotPages.length,
setConfigData: seedConfig,
),
);
return page.copyWith(slotPages: newSlotPages);
}).toList();
state = state.copyWith(pages: updatedPages);
recalculateIndices();
_logger.fine('Added set to page $pageUUID');
}

/// Removes a whole exercise (set [PageEntry]) from the session.
///
/// No-op when it would leave the session with no exercises — there must
/// always be at least one exercise page to log against.
void removeExercisePage(String pageUUID) {
final setPages = state.pages.where((p) => p.type == PageType.set).toList();
if (setPages.length <= 1) {
_logger.warning('Refusing to remove the last exercise from page $pageUUID');
return;
}
final updatedPages = state.pages.where((page) {
return !(page.type == PageType.set && page.uuid == pageUUID);
}).toList();
state = state.copyWith(pages: updatedPages);
recalculateIndices();
_logger.fine('Removed exercise page $pageUUID');
}

void removeSetFromPage(String pageUUID, String slotUUID) {
final updatedPages = state.pages.map((page) {
if (page.type != PageType.set || page.uuid != pageUUID) {
return page;
}
final updatedSlotPages = page.slotPages.where((sp) => sp.uuid != slotUUID).toList();
return page.copyWith(slotPages: updatedSlotPages);
}).toList();
state = state.copyWith(pages: updatedPages);
recalculateIndices();
_logger.fine('Removed set $slotUUID from page $pageUUID');
}

/// Resets the workout start time to now, e.g. when the user taps "start"
void startWorkout() {
_logger.fine('Setting workout start time');
Expand All @@ -476,6 +528,7 @@ class GymStateNotifier extends _$GymStateNotifier {

void clear() {
_logger.fine('Clearing state');
ref.read(restTimerProvider.notifier).cancel();
state = state.copyWith(
isInitialized: false,
pages: [],
Expand Down
2 changes: 1 addition & 1 deletion lib/features/routines/providers/gym_state_notifier.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading