Skip to content
Open
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
7 changes: 7 additions & 0 deletions lib/features/routines/providers/gym_log_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ class GymLogNotifier extends _$GymLogNotifier {
state = out;
}

/// Starts editing a log that was already persisted during the current
/// workout. Unlike [setLog], the id, session and date are kept so saving
/// updates the existing row instead of inserting a duplicate.
void editLog(Log log) {
state = log;
}

void setWeight(num weight) {
state = state?.copyWith(weight: weight);
}
Expand Down
32 changes: 32 additions & 0 deletions lib/features/routines/providers/gym_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'package:flutter/material.dart';
import 'package:wger/core/uuid.dart';
import 'package:wger/features/exercises/models/exercise.dart';
import 'package:wger/features/routines/models/day_data.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';

Expand Down Expand Up @@ -118,6 +119,16 @@ class SlotPageEntry {
/// Whether the log page has been marked as done
final bool logDone;

/// The log saved from this page during the current workout, if any. Kept
/// so revisiting the page pre-fills the form with the saved values and
/// saving updates the row instead of inserting a duplicate.
final Log? loggedEntry;

/// When the timer of this page was started. Lives in the state (not the
/// timer widget) so the countdown continues when the user navigates away
/// and back.
final DateTime? timerStartedAt;

/// The associated SetConfigData
final SetConfigData? setConfigData;

Expand All @@ -127,6 +138,8 @@ class SlotPageEntry {
required this.setIndex,
this.setConfigData,
this.logDone = false,
this.loggedEntry,
this.timerStartedAt,
String? uuid,
}) : assert(
type != SlotPageType.log || setConfigData != null,
Expand All @@ -142,6 +155,8 @@ class SlotPageEntry {
int? pageIndex,
SetConfigData? setConfigData,
bool? logDone,
Log? loggedEntry,
DateTime? timerStartedAt,
}) {
return SlotPageEntry(
uuid: uuid ?? this.uuid,
Expand All @@ -150,6 +165,8 @@ class SlotPageEntry {
pageIndex: pageIndex ?? this.pageIndex,
setConfigData: setConfigData ?? this.setConfigData,
logDone: logDone ?? this.logDone,
loggedEntry: loggedEntry ?? this.loggedEntry,
timerStartedAt: timerStartedAt ?? this.timerStartedAt,
);
}

Expand Down Expand Up @@ -326,6 +343,21 @@ class GymModeState {
return null;
}

/// Returns the next log page after the slot page at [pageIndex], i.e. the
/// set that will be performed once the current rest timer ends.
///
/// This deliberately returns the next *set*, which may belong to the same
/// exercise (e.g. when progressing weights within an exercise) or to the
/// next one. Returns null when the workout has no further sets.
SlotPageEntry? getNextLogPage(int pageIndex) {
for (final slotPage in pages.expand((p) => p.slotPages)) {
if (slotPage.pageIndex > pageIndex && slotPage.type == SlotPageType.log) {
return slotPage;
}
}
return null;
}

double get ratioCompleted {
if (totalPages == 0) {
return 0.0;
Expand Down
55 changes: 46 additions & 9 deletions lib/features/routines/providers/gym_state_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,15 @@ class GymStateNotifier extends _$GymStateNotifier {
return;
}

// Re-visiting a page that was already logged in this workout: seed the
// form with the saved entry (keeping its id and session) so saving
// updates the row instead of inserting a duplicate.
final loggedEntry = slotEntryPage.loggedEntry;
if (loggedEntry != null) {
ref.read(gymLogProvider.notifier).editLog(loggedEntry);
return;
}

final log = Log.fromSetConfigData(
slotEntryPage.setConfigData!,
routineId: state.routine.id,
Expand Down Expand Up @@ -365,15 +374,8 @@ class GymStateNotifier extends _$GymStateNotifier {
_savePrefs();
}

void markSlotPageAsDone(String uuid, {required bool isDone}) {
final slotPage = state.getSlotPageByUUID(uuid);
if (slotPage == null) {
_logger.warning('No slot page found for UUID $uuid');
return;
}

final updatedSlotPage = slotPage.copyWith(logDone: isDone);

/// Replaces the slot page with [uuid] by [updatedSlotPage] in the state.
void _updateSlotPage(String uuid, SlotPageEntry updatedSlotPage) {
final updatedPages = state.pages.map((page) {
if (page.type != PageType.set) {
return page;
Expand All @@ -390,6 +392,41 @@ class GymStateNotifier extends _$GymStateNotifier {
}).toList();

state = state.copyWith(pages: updatedPages);
}

/// Starts the timer of the slot page with [uuid], unless it is already
/// running.
///
/// The start time lives in the gym state (not the timer widget) so the
/// countdown continues when the page is disposed and re-created while
/// navigating, e.g. when going back to correct a logged set during the
/// rest period.
void startTimerIfNeeded(String uuid) {
final slotPage = state.getSlotPageByUUID(uuid);
if (slotPage == null) {
_logger.warning('No slot page found for UUID $uuid');
return;
}
if (slotPage.timerStartedAt != null) {
return;
}

_updateSlotPage(uuid, slotPage.copyWith(timerStartedAt: clock.now()));
_logger.fine('Started timer for slot page UUID $uuid');
}

/// [log] is the entry saved from this page during the current workout. It
/// is kept on the slot page so revisiting the page pre-fills the form with
/// the saved values and saving updates the row instead of inserting a
/// duplicate.
void markSlotPageAsDone(String uuid, {required bool isDone, Log? log}) {
final slotPage = state.getSlotPageByUUID(uuid);
if (slotPage == null) {
_logger.warning('No slot page found for UUID $uuid');
return;
}

_updateSlotPage(uuid, slotPage.copyWith(logDone: isDone, loggedEntry: log));
_logger.fine('Set logDone=$isDone for slot page UUID $uuid');
}

Expand Down
3 changes: 2 additions & 1 deletion lib/features/routines/widgets/gym_mode/gym_mode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,9 @@ class _GymModeState extends ConsumerState<GymMode> {
? TimerCountdownWidget(
_controller,
(rest ?? gymState.countdownDuration.inSeconds).toInt(),
slotPage.uuid,
)
: TimerWidget(_controller),
: TimerWidget(_controller, slotPage.uuid),
);
}
}
Expand Down
16 changes: 14 additions & 2 deletions lib/features/routines/widgets/gym_mode/log_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -393,12 +393,24 @@ class _LogFormWidgetState extends ConsumerState<LogFormWidget> {

// A failed write is intentionally left to propagate to the global
// error handler; the success path below is then skipped.
await logProvider.addEntry(log);
//
// Re-saving a page that was already logged in this workout
// updates the existing row instead of inserting a duplicate.
// The id/session can be missing on the form log, e.g. after
// copying values from a past log, so restore them first.
final existingEntry = page.loggedEntry;
if (existingEntry == null) {
await logProvider.addEntry(log);
} else {
log.id = existingEntry.id;
log.sessionId = existingEntry.sessionId;
await logProvider.updateEntry(log);
}
if (!context.mounted) {
return;
}

gymProvider.markSlotPageAsDone(page.uuid, isDone: true);
gymProvider.markSlotPageAsDone(page.uuid, isDone: true, log: log);
showSnackbar(
context,
i18n.successfullySaved,
Expand Down
Loading