feat(exécution): refonte des chronos de séance et carte exercice actif (ticket #90)
Réorganise l'écran d'exécution autour d'une carte "Exercice actif" sous le compteur SÉRIE X/Y (nom, médias, temps de série, action "Démarrer l'exercice"). Le timer de série devient un état persistant dédié (plus un DateTime UI volatile), pause-aware, et "Démarrer l'exercice" lance en une action tous les chronos qui doivent démarrer en début de série (série, première étape temps, score chrono si stopwatch). Implémentation restée non commitée depuis sa réalisation ; QA a validé les vérifications statiques exécutables en sandbox (dart analyze, git diff --check). flutter analyze / flutter test restent à relancer dans un environnement avec cache Flutter SDK accessible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -388,6 +388,7 @@ abstract interface class ActiveSessionRepository {
|
||||
Future<ActiveWorkoutSession?> findOpen();
|
||||
Future<void> save(ActiveWorkoutSession session);
|
||||
Future<void> saveSetResult(ActiveSetResult result);
|
||||
Future<void> saveSetTimerState(ActiveSetTimerState state);
|
||||
Future<void> saveRestState(ActiveRestState restState);
|
||||
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state);
|
||||
Future<void> saveExerciseStepProgressState(
|
||||
@ -408,6 +409,12 @@ abstract interface class ActiveSessionRepository {
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
});
|
||||
Future<ActiveSetTimerState?> findSetTimerState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
});
|
||||
Future<ActiveExerciseStepProgressState?> findExerciseStepProgressState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
@ -415,6 +422,7 @@ abstract interface class ActiveSessionRepository {
|
||||
required int setIndex,
|
||||
});
|
||||
Future<List<ActiveSetResult>> listSetResults(String sessionId);
|
||||
Future<List<ActiveSetTimerState>> listSetTimerStates(String sessionId);
|
||||
Future<List<ActiveExerciseStepProgressState>> listExerciseStepProgressStates(
|
||||
String sessionId,
|
||||
);
|
||||
|
||||
@ -1601,6 +1601,18 @@ final class WorkoutTemplateExerciseOverrideConfig {
|
||||
final bool? autoStartNextTimedStepOverride;
|
||||
}
|
||||
|
||||
final class SetExecutionTimerStartResult {
|
||||
const SetExecutionTimerStartResult({
|
||||
this.setTimer,
|
||||
this.scoreStopwatch,
|
||||
this.stepProgress,
|
||||
});
|
||||
|
||||
final ActiveSetTimerState? setTimer;
|
||||
final ActiveScoreStopwatchState? scoreStopwatch;
|
||||
final ActiveExerciseStepProgressState? stepProgress;
|
||||
}
|
||||
|
||||
final class ActiveWorkoutSessionUseCases {
|
||||
const ActiveWorkoutSessionUseCases({
|
||||
required this.sessionRepository,
|
||||
@ -1697,13 +1709,25 @@ final class ActiveWorkoutSessionUseCases {
|
||||
sessionId,
|
||||
)).where((state) => state.status == ActiveScoreStopwatchStatus.running);
|
||||
for (final state in runningStopwatches) {
|
||||
final stopped = state.copyWith(
|
||||
final pausedStopwatch = state.copyWith(
|
||||
metadata: state.metadata.touch(now),
|
||||
status: ActiveScoreStopwatchStatus.stopped,
|
||||
status: ActiveScoreStopwatchStatus.paused,
|
||||
accumulatedMs: state.elapsedMillisecondsAt(now),
|
||||
stoppedAt: now,
|
||||
);
|
||||
await sessionRepository.saveScoreStopwatchState(stopped);
|
||||
await sessionRepository.saveScoreStopwatchState(pausedStopwatch);
|
||||
}
|
||||
final runningSetTimers = (await sessionRepository.listSetTimerStates(
|
||||
sessionId,
|
||||
)).where((state) => state.status == ActiveSetTimerStatus.running);
|
||||
for (final state in runningSetTimers) {
|
||||
final pausedSetTimer = state.copyWith(
|
||||
metadata: state.metadata.touch(now),
|
||||
status: ActiveSetTimerStatus.paused,
|
||||
startedAt: null,
|
||||
accumulatedMs: state.elapsedMillisecondsAt(now),
|
||||
);
|
||||
await sessionRepository.saveSetTimerState(pausedSetTimer);
|
||||
}
|
||||
final runningStepTimers =
|
||||
(await sessionRepository.listExerciseStepProgressStates(
|
||||
@ -1722,6 +1746,18 @@ final class ActiveWorkoutSessionUseCases {
|
||||
);
|
||||
await sessionRepository.saveExerciseStepProgressState(pausedStep);
|
||||
}
|
||||
final activeRests = (await sessionRepository.listRestStates(sessionId))
|
||||
.where(
|
||||
(rest) =>
|
||||
rest.endedAt == null &&
|
||||
rest.skippedAt == null &&
|
||||
rest.pausedAt == null,
|
||||
);
|
||||
for (final rest in activeRests) {
|
||||
await sessionRepository.saveRestState(
|
||||
rest.copyWith(metadata: rest.metadata.touch(now), pausedAt: now),
|
||||
);
|
||||
}
|
||||
final paused = session.pause(now);
|
||||
await sessionRepository.save(paused);
|
||||
return paused;
|
||||
@ -1729,7 +1765,68 @@ final class ActiveWorkoutSessionUseCases {
|
||||
|
||||
Future<ActiveWorkoutSession> resume(String sessionId) async {
|
||||
final session = await _requiredSession(sessionId);
|
||||
final resumed = session.resume(clock.now());
|
||||
final now = clock.now();
|
||||
final pausedSetTimers = (await sessionRepository.listSetTimerStates(
|
||||
sessionId,
|
||||
)).where((state) => state.status == ActiveSetTimerStatus.paused);
|
||||
for (final state in pausedSetTimers) {
|
||||
await sessionRepository.saveSetTimerState(
|
||||
state.copyWith(
|
||||
metadata: state.metadata.touch(now),
|
||||
status: ActiveSetTimerStatus.running,
|
||||
startedAt: now,
|
||||
),
|
||||
);
|
||||
}
|
||||
final pausedStopwatches = (await sessionRepository.listScoreStopwatchStates(
|
||||
sessionId,
|
||||
)).where((state) => state.status == ActiveScoreStopwatchStatus.paused);
|
||||
for (final state in pausedStopwatches) {
|
||||
await sessionRepository.saveScoreStopwatchState(
|
||||
state.copyWith(
|
||||
metadata: state.metadata.touch(now),
|
||||
status: ActiveScoreStopwatchStatus.running,
|
||||
startedAt: now,
|
||||
stoppedAt: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
final sessionPausedAt = session.pausedAt;
|
||||
final pausedStepTimers =
|
||||
(await sessionRepository.listExerciseStepProgressStates(
|
||||
sessionId,
|
||||
)).where(
|
||||
(state) =>
|
||||
state.status == ActiveExerciseStepProgressStatus.pausedTimer &&
|
||||
sessionPausedAt != null &&
|
||||
state.lastTransitionAt.isAtSameMomentAs(sessionPausedAt),
|
||||
);
|
||||
for (final state in pausedStepTimers) {
|
||||
await sessionRepository.saveExerciseStepProgressState(
|
||||
state.copyWith(
|
||||
metadata: state.metadata.touch(now),
|
||||
status: ActiveExerciseStepProgressStatus.runningTimer,
|
||||
startedAt: now,
|
||||
lastTransitionAt: now,
|
||||
),
|
||||
);
|
||||
}
|
||||
final pausedRests = (await sessionRepository.listRestStates(
|
||||
sessionId,
|
||||
)).where((rest) => rest.pausedAt != null);
|
||||
for (final rest in pausedRests) {
|
||||
final pausedAt = rest.pausedAt!;
|
||||
await sessionRepository.saveRestState(
|
||||
rest.copyWith(
|
||||
metadata: rest.metadata.touch(now),
|
||||
pausedAt: null,
|
||||
accumulatedPausedMs:
|
||||
rest.accumulatedPausedMs +
|
||||
now.difference(pausedAt).inMilliseconds,
|
||||
),
|
||||
);
|
||||
}
|
||||
final resumed = session.resume(now);
|
||||
await sessionRepository.save(resumed);
|
||||
return resumed;
|
||||
}
|
||||
@ -1738,6 +1835,182 @@ final class ActiveWorkoutSessionUseCases {
|
||||
return session.elapsedActiveMillisecondsAt(clock.now());
|
||||
}
|
||||
|
||||
Future<SetExecutionTimerStartResult> startSetExecution({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
final session = await _requiredSession(sessionId);
|
||||
final snapshot = _findExerciseSnapshot(
|
||||
resolvedTemplateSnapshotJson: session.resolvedTemplateSnapshotJson,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
);
|
||||
if (snapshot == null) {
|
||||
throw const DomainException('Exercise position not found in session.');
|
||||
}
|
||||
final now = clock.now();
|
||||
final setTimer = snapshot.timeEnabled
|
||||
? await _startOrResumeSetTimer(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
now: now,
|
||||
)
|
||||
: null;
|
||||
final scoreStopwatch =
|
||||
snapshot.scoreEnabled &&
|
||||
snapshot.scoreInputModeSnapshot == ScoreInputMode.stopwatch
|
||||
? await _startScoreStopwatchForSetExecution(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
now: now,
|
||||
)
|
||||
: null;
|
||||
final stepProgress = await _startFirstStepTimerIfNeeded(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
snapshot: snapshot,
|
||||
);
|
||||
return SetExecutionTimerStartResult(
|
||||
setTimer: setTimer,
|
||||
scoreStopwatch: scoreStopwatch,
|
||||
stepProgress: stepProgress,
|
||||
);
|
||||
}
|
||||
|
||||
Future<SetExecutionTimerStartResult> startCurrentExerciseTimers({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) {
|
||||
return startSetExecution(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ActiveSetTimerState?> stopSetExecutionTimers({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
final now = clock.now();
|
||||
final setTimer = await _finishSetTimer(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
status: ActiveSetTimerStatus.stopped,
|
||||
now: now,
|
||||
);
|
||||
final scoreStopwatch = await sessionRepository.findScoreStopwatchState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
if (scoreStopwatch != null &&
|
||||
scoreStopwatch.status != ActiveScoreStopwatchStatus.stopped) {
|
||||
await sessionRepository.saveScoreStopwatchState(
|
||||
scoreStopwatch.copyWith(
|
||||
metadata: scoreStopwatch.metadata.touch(now),
|
||||
status: ActiveScoreStopwatchStatus.stopped,
|
||||
accumulatedMs: scoreStopwatch.elapsedMillisecondsAt(now),
|
||||
stoppedAt: now,
|
||||
),
|
||||
);
|
||||
}
|
||||
final stepProgress = await sessionRepository.findExerciseStepProgressState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
if (stepProgress?.status == ActiveExerciseStepProgressStatus.runningTimer) {
|
||||
await sessionRepository.saveExerciseStepProgressState(
|
||||
stepProgress!.copyWith(
|
||||
metadata: stepProgress.metadata.touch(now),
|
||||
status: ActiveExerciseStepProgressStatus.pausedTimer,
|
||||
startedAt: null,
|
||||
accumulatedMs: stepProgress.elapsedMillisecondsAt(now),
|
||||
lastTransitionAt: now,
|
||||
),
|
||||
);
|
||||
}
|
||||
return setTimer;
|
||||
}
|
||||
|
||||
Future<ActiveSetTimerState?> skipSetExecutionTimers({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
final now = clock.now();
|
||||
final setTimer = await _finishSetTimer(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
status: ActiveSetTimerStatus.skipped,
|
||||
now: now,
|
||||
);
|
||||
final scoreStopwatch = await sessionRepository.findScoreStopwatchState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
if (scoreStopwatch != null &&
|
||||
scoreStopwatch.status != ActiveScoreStopwatchStatus.stopped) {
|
||||
await sessionRepository.saveScoreStopwatchState(
|
||||
scoreStopwatch.copyWith(
|
||||
metadata: scoreStopwatch.metadata.touch(now),
|
||||
status: ActiveScoreStopwatchStatus.stopped,
|
||||
accumulatedMs: scoreStopwatch.elapsedMillisecondsAt(now),
|
||||
stoppedAt: now,
|
||||
),
|
||||
);
|
||||
}
|
||||
final stepProgress = await sessionRepository.findExerciseStepProgressState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
if (stepProgress != null &&
|
||||
stepProgress.status !=
|
||||
ActiveExerciseStepProgressStatus.sequenceComplete) {
|
||||
try {
|
||||
await ActiveExerciseStepUseCases(
|
||||
sessionRepository: sessionRepository,
|
||||
clock: clock,
|
||||
ids: ids,
|
||||
originDeviceId: originDeviceId,
|
||||
).skipSequence(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
} on DomainException {
|
||||
// A set without configured steps has no step sequence to skip.
|
||||
}
|
||||
}
|
||||
return setTimer;
|
||||
}
|
||||
|
||||
Future<ActiveSetResult> recordSetResult(ActiveSetResult result) async {
|
||||
await sessionRepository.saveSetResult(result);
|
||||
return result;
|
||||
@ -1981,6 +2254,24 @@ final class ActiveWorkoutSessionUseCases {
|
||||
return state.elapsedMillisecondsAt(clock.now());
|
||||
}
|
||||
|
||||
Future<ActiveSetTimerState?> findSetTimer({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) {
|
||||
return sessionRepository.findSetTimerState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
}
|
||||
|
||||
int setTimerElapsedMilliseconds(ActiveSetTimerState state) {
|
||||
return state.elapsedMillisecondsAt(clock.now());
|
||||
}
|
||||
|
||||
Future<List<SetResultPositionState>> listSetResults(String sessionId) async {
|
||||
final session = await _requiredSession(sessionId);
|
||||
final snapshots = _listSetSnapshots(session.resolvedTemplateSnapshotJson);
|
||||
@ -2140,6 +2431,158 @@ final class ActiveWorkoutSessionUseCases {
|
||||
return active.isEmpty ? null : active.first;
|
||||
}
|
||||
|
||||
Future<ActiveSetTimerState> _startOrResumeSetTimer({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
required DateTime now,
|
||||
}) async {
|
||||
final existing = await sessionRepository.findSetTimerState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
if (existing != null) {
|
||||
if (existing.status == ActiveSetTimerStatus.running ||
|
||||
existing.status == ActiveSetTimerStatus.stopped ||
|
||||
existing.status == ActiveSetTimerStatus.skipped) {
|
||||
return existing;
|
||||
}
|
||||
final resumed = existing.copyWith(
|
||||
metadata: existing.metadata.touch(now),
|
||||
status: ActiveSetTimerStatus.running,
|
||||
startedAt: now,
|
||||
stoppedAt: null,
|
||||
skippedAt: null,
|
||||
);
|
||||
await sessionRepository.saveSetTimerState(resumed);
|
||||
return resumed;
|
||||
}
|
||||
final state = ActiveSetTimerState(
|
||||
metadata: _newMetadata(ids, originDeviceId, now),
|
||||
activeWorkoutSessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
status: ActiveSetTimerStatus.running,
|
||||
startedAt: now,
|
||||
accumulatedMs: 0,
|
||||
);
|
||||
await sessionRepository.saveSetTimerState(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
Future<ActiveSetTimerState?> _finishSetTimer({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
required ActiveSetTimerStatus status,
|
||||
required DateTime now,
|
||||
}) async {
|
||||
final existing = await sessionRepository.findSetTimerState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
if (existing == null) {
|
||||
return null;
|
||||
}
|
||||
if (existing.status == status) {
|
||||
return existing;
|
||||
}
|
||||
final finished = existing.copyWith(
|
||||
metadata: existing.metadata.touch(now),
|
||||
status: status,
|
||||
startedAt: null,
|
||||
accumulatedMs: status == ActiveSetTimerStatus.skipped
|
||||
? existing.accumulatedMs
|
||||
: existing.elapsedMillisecondsAt(now),
|
||||
stoppedAt: status == ActiveSetTimerStatus.stopped ? now : null,
|
||||
skippedAt: status == ActiveSetTimerStatus.skipped ? now : null,
|
||||
);
|
||||
await sessionRepository.saveSetTimerState(finished);
|
||||
return finished;
|
||||
}
|
||||
|
||||
Future<ActiveScoreStopwatchState> _startScoreStopwatchForSetExecution({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
required DateTime now,
|
||||
}) async {
|
||||
final existing = await sessionRepository.findScoreStopwatchState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
if (existing != null) {
|
||||
if (existing.status == ActiveScoreStopwatchStatus.running ||
|
||||
existing.status == ActiveScoreStopwatchStatus.stopped) {
|
||||
return existing;
|
||||
}
|
||||
final resumed = existing.copyWith(
|
||||
metadata: existing.metadata.touch(now),
|
||||
status: ActiveScoreStopwatchStatus.running,
|
||||
startedAt: now,
|
||||
stoppedAt: null,
|
||||
);
|
||||
await sessionRepository.saveScoreStopwatchState(resumed);
|
||||
return resumed;
|
||||
}
|
||||
final state = ActiveScoreStopwatchState(
|
||||
metadata: _newMetadata(ids, originDeviceId, now),
|
||||
activeWorkoutSessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
status: ActiveScoreStopwatchStatus.running,
|
||||
startedAt: now,
|
||||
accumulatedMs: 0,
|
||||
);
|
||||
await sessionRepository.saveScoreStopwatchState(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
Future<ActiveExerciseStepProgressState?> _startFirstStepTimerIfNeeded({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
required _ResolvedExerciseSnapshot snapshot,
|
||||
}) async {
|
||||
if (snapshot.steps.isEmpty ||
|
||||
snapshot.steps.first.type != ExerciseStepType.time) {
|
||||
return null;
|
||||
}
|
||||
final existing = await sessionRepository.findExerciseStepProgressState(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
if (existing != null &&
|
||||
(existing.currentPassageIndex != 0 || existing.currentStepIndex != 0)) {
|
||||
return existing;
|
||||
}
|
||||
return ActiveExerciseStepUseCases(
|
||||
sessionRepository: sessionRepository,
|
||||
clock: clock,
|
||||
ids: ids,
|
||||
originDeviceId: originDeviceId,
|
||||
).startTimer(
|
||||
sessionId: sessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
}
|
||||
|
||||
Future<ActiveWorkoutSession> _requiredSession(String sessionId) async {
|
||||
final session = await sessionRepository.findById(sessionId);
|
||||
if (session == null) {
|
||||
|
||||
@ -14,7 +14,9 @@ enum ActiveWorkoutStatus { running, paused, savedExit, completed, abandoned }
|
||||
|
||||
enum SetResultStatus { completed, skipped }
|
||||
|
||||
enum ActiveScoreStopwatchStatus { running, stopped }
|
||||
enum ActiveSetTimerStatus { running, paused, stopped, skipped }
|
||||
|
||||
enum ActiveScoreStopwatchStatus { running, paused, stopped }
|
||||
|
||||
enum ShareResourceType { program, workoutTemplate }
|
||||
|
||||
@ -1031,7 +1033,7 @@ final class ActiveScoreStopwatchState {
|
||||
final DateTime? stoppedAt;
|
||||
|
||||
int elapsedMillisecondsAt(DateTime now) {
|
||||
if (status == ActiveScoreStopwatchStatus.stopped) {
|
||||
if (status != ActiveScoreStopwatchStatus.running) {
|
||||
return accumulatedMs;
|
||||
}
|
||||
return accumulatedMs + now.difference(startedAt).inMilliseconds;
|
||||
@ -1060,6 +1062,80 @@ final class ActiveScoreStopwatchState {
|
||||
}
|
||||
}
|
||||
|
||||
final class ActiveSetTimerState {
|
||||
ActiveSetTimerState({
|
||||
required this.metadata,
|
||||
required this.activeWorkoutSessionId,
|
||||
required this.programIndex,
|
||||
required this.exerciseIndex,
|
||||
required this.setIndex,
|
||||
required this.status,
|
||||
this.startedAt,
|
||||
required this.accumulatedMs,
|
||||
this.stoppedAt,
|
||||
this.skippedAt,
|
||||
}) {
|
||||
_requireNonNegative(programIndex, 'Program index');
|
||||
_requireNonNegative(exerciseIndex, 'Exercise index');
|
||||
_requireNonNegative(setIndex, 'Set index');
|
||||
_requireNonNegative(accumulatedMs, 'Accumulated milliseconds');
|
||||
if (status == ActiveSetTimerStatus.running && startedAt == null) {
|
||||
throw const DomainException(
|
||||
'Running set timer requires a start timestamp.',
|
||||
);
|
||||
}
|
||||
if (stoppedAt != null && skippedAt != null) {
|
||||
throw const DomainException('Set timer cannot be stopped and skipped.');
|
||||
}
|
||||
}
|
||||
|
||||
final EntityMetadata metadata;
|
||||
final String activeWorkoutSessionId;
|
||||
final int programIndex;
|
||||
final int exerciseIndex;
|
||||
final int setIndex;
|
||||
final ActiveSetTimerStatus status;
|
||||
final DateTime? startedAt;
|
||||
final int accumulatedMs;
|
||||
final DateTime? stoppedAt;
|
||||
final DateTime? skippedAt;
|
||||
|
||||
int elapsedMillisecondsAt(DateTime now) {
|
||||
if (status != ActiveSetTimerStatus.running || startedAt == null) {
|
||||
return accumulatedMs;
|
||||
}
|
||||
return accumulatedMs + now.difference(startedAt!).inMilliseconds;
|
||||
}
|
||||
|
||||
ActiveSetTimerState copyWith({
|
||||
EntityMetadata? metadata,
|
||||
ActiveSetTimerStatus? status,
|
||||
Object? startedAt = _unchanged,
|
||||
int? accumulatedMs,
|
||||
Object? stoppedAt = _unchanged,
|
||||
Object? skippedAt = _unchanged,
|
||||
}) {
|
||||
return ActiveSetTimerState(
|
||||
metadata: metadata ?? this.metadata,
|
||||
activeWorkoutSessionId: activeWorkoutSessionId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
status: status ?? this.status,
|
||||
startedAt: startedAt == _unchanged
|
||||
? this.startedAt
|
||||
: startedAt as DateTime?,
|
||||
accumulatedMs: accumulatedMs ?? this.accumulatedMs,
|
||||
stoppedAt: stoppedAt == _unchanged
|
||||
? this.stoppedAt
|
||||
: stoppedAt as DateTime?,
|
||||
skippedAt: skippedAt == _unchanged
|
||||
? this.skippedAt
|
||||
: skippedAt as DateTime?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final class ActiveRestState {
|
||||
const ActiveRestState({
|
||||
required this.metadata,
|
||||
@ -1072,6 +1148,8 @@ final class ActiveRestState {
|
||||
required this.startedAt,
|
||||
this.endedAt,
|
||||
this.skippedAt,
|
||||
this.pausedAt,
|
||||
this.accumulatedPausedMs = 0,
|
||||
});
|
||||
|
||||
final EntityMetadata metadata;
|
||||
@ -1084,12 +1162,28 @@ final class ActiveRestState {
|
||||
final DateTime startedAt;
|
||||
final DateTime? endedAt;
|
||||
final DateTime? skippedAt;
|
||||
final DateTime? pausedAt;
|
||||
final int accumulatedPausedMs;
|
||||
|
||||
int elapsedMillisecondsAt(DateTime now) {
|
||||
final effectiveNow = pausedAt ?? now;
|
||||
final elapsed =
|
||||
effectiveNow.difference(startedAt).inMilliseconds - accumulatedPausedMs;
|
||||
return elapsed < 0 ? 0 : elapsed;
|
||||
}
|
||||
|
||||
int remainingMillisecondsAt(DateTime now) {
|
||||
final remaining = adjustedRestSeconds * 1000 - elapsedMillisecondsAt(now);
|
||||
return remaining < 0 ? 0 : remaining;
|
||||
}
|
||||
|
||||
ActiveRestState copyWith({
|
||||
EntityMetadata? metadata,
|
||||
int? adjustedRestSeconds,
|
||||
Object? endedAt = _unchanged,
|
||||
Object? skippedAt = _unchanged,
|
||||
Object? pausedAt = _unchanged,
|
||||
int? accumulatedPausedMs,
|
||||
}) {
|
||||
return ActiveRestState(
|
||||
metadata: metadata ?? this.metadata,
|
||||
@ -1104,6 +1198,8 @@ final class ActiveRestState {
|
||||
skippedAt: skippedAt == _unchanged
|
||||
? this.skippedAt
|
||||
: skippedAt as DateTime?,
|
||||
pausedAt: pausedAt == _unchanged ? this.pausedAt : pausedAt as DateTime?,
|
||||
accumulatedPausedMs: accumulatedPausedMs ?? this.accumulatedPausedMs,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ part 'app_database.g.dart';
|
||||
ActiveExerciseStepResults,
|
||||
ActiveRestStates,
|
||||
ActiveScoreStopwatchStates,
|
||||
ActiveSetTimerStates,
|
||||
ActiveSetResults,
|
||||
ActiveWorkoutSessions,
|
||||
ChangeLogEntries,
|
||||
@ -46,13 +47,14 @@ final class AppDatabase extends _$AppDatabase {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 14;
|
||||
int get schemaVersion => 15;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onCreate: (migrator) async {
|
||||
await migrator.createAll();
|
||||
await _migrateToSchema15();
|
||||
await _createIndexes();
|
||||
},
|
||||
onUpgrade: (migrator, from, to) async {
|
||||
@ -103,6 +105,9 @@ final class AppDatabase extends _$AppDatabase {
|
||||
if (from < 14) {
|
||||
await _migrateToSchema14();
|
||||
}
|
||||
if (from < 15) {
|
||||
await _migrateToSchema15();
|
||||
}
|
||||
await _createIndexes();
|
||||
},
|
||||
beforeOpen: (details) async {
|
||||
@ -171,6 +176,10 @@ final class AppDatabase extends _$AppDatabase {
|
||||
'CREATE INDEX IF NOT EXISTS idx_active_score_stopwatch_states_session_id '
|
||||
'ON active_score_stopwatch_states (active_workout_session_id)',
|
||||
);
|
||||
await customStatement(
|
||||
'CREATE INDEX IF NOT EXISTS idx_active_set_timer_states_session_id '
|
||||
'ON active_set_timer_states (active_workout_session_id)',
|
||||
);
|
||||
await customStatement(
|
||||
'CREATE INDEX IF NOT EXISTS idx_active_rest_states_session_id '
|
||||
'ON active_rest_states (active_workout_session_id)',
|
||||
@ -223,6 +232,7 @@ const _syncableTableNames = [
|
||||
'active_exercise_step_results',
|
||||
'active_rest_states',
|
||||
'active_score_stopwatch_states',
|
||||
'active_set_timer_states',
|
||||
'active_set_results',
|
||||
'active_workout_sessions',
|
||||
'exercises',
|
||||
@ -494,4 +504,141 @@ FROM exercises
|
||||
'CHECK (auto_start_next_timed_step_override IN (0, 1))',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _migrateToSchema15() async {
|
||||
await customStatement('PRAGMA foreign_keys = OFF');
|
||||
await customStatement('''
|
||||
CREATE TABLE active_score_stopwatch_states_new (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
deleted_at INTEGER,
|
||||
schema_version INTEGER NOT NULL DEFAULT 1,
|
||||
sync_state TEXT NOT NULL CHECK (sync_state IN ('localOnly', 'dirty', 'synced', 'deleted')),
|
||||
local_revision INTEGER NOT NULL CHECK (local_revision >= 0),
|
||||
origin_device_id TEXT NOT NULL,
|
||||
future_owner_profile_id TEXT,
|
||||
last_synced_at INTEGER,
|
||||
remote_revision TEXT,
|
||||
active_workout_session_id TEXT NOT NULL REFERENCES active_workout_sessions(id),
|
||||
program_index INTEGER NOT NULL,
|
||||
exercise_index INTEGER NOT NULL,
|
||||
set_index INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
started_at INTEGER NOT NULL,
|
||||
accumulated_ms INTEGER NOT NULL,
|
||||
stopped_at INTEGER,
|
||||
UNIQUE (active_workout_session_id, program_index, exercise_index, set_index),
|
||||
CHECK (length(trim(origin_device_id)) > 0),
|
||||
CHECK (future_owner_profile_id IS NULL OR length(trim(future_owner_profile_id)) > 0),
|
||||
CHECK (remote_revision IS NULL OR length(trim(remote_revision)) > 0),
|
||||
CHECK (program_index >= 0),
|
||||
CHECK (exercise_index >= 0),
|
||||
CHECK (set_index >= 0),
|
||||
CHECK (status IN ('running', 'paused', 'stopped')),
|
||||
CHECK (accumulated_ms >= 0)
|
||||
)
|
||||
''');
|
||||
await customStatement('''
|
||||
INSERT INTO active_score_stopwatch_states_new (
|
||||
id,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at,
|
||||
schema_version,
|
||||
sync_state,
|
||||
local_revision,
|
||||
origin_device_id,
|
||||
future_owner_profile_id,
|
||||
last_synced_at,
|
||||
remote_revision,
|
||||
active_workout_session_id,
|
||||
program_index,
|
||||
exercise_index,
|
||||
set_index,
|
||||
status,
|
||||
started_at,
|
||||
accumulated_ms,
|
||||
stopped_at
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
created_at,
|
||||
updated_at,
|
||||
deleted_at,
|
||||
schema_version,
|
||||
sync_state,
|
||||
local_revision,
|
||||
origin_device_id,
|
||||
future_owner_profile_id,
|
||||
last_synced_at,
|
||||
remote_revision,
|
||||
active_workout_session_id,
|
||||
program_index,
|
||||
exercise_index,
|
||||
set_index,
|
||||
status,
|
||||
started_at,
|
||||
accumulated_ms,
|
||||
stopped_at
|
||||
FROM active_score_stopwatch_states
|
||||
''');
|
||||
await customStatement('DROP TABLE active_score_stopwatch_states');
|
||||
await customStatement(
|
||||
'ALTER TABLE active_score_stopwatch_states_new '
|
||||
'RENAME TO active_score_stopwatch_states',
|
||||
);
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
await customStatement('''
|
||||
CREATE TABLE IF NOT EXISTS active_set_timer_states (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
deleted_at INTEGER,
|
||||
schema_version INTEGER NOT NULL DEFAULT 1,
|
||||
sync_state TEXT NOT NULL CHECK (sync_state IN ('localOnly', 'dirty', 'synced', 'deleted')),
|
||||
local_revision INTEGER NOT NULL CHECK (local_revision >= 0),
|
||||
origin_device_id TEXT NOT NULL,
|
||||
future_owner_profile_id TEXT,
|
||||
last_synced_at INTEGER,
|
||||
remote_revision TEXT,
|
||||
active_workout_session_id TEXT NOT NULL REFERENCES active_workout_sessions(id),
|
||||
program_index INTEGER NOT NULL,
|
||||
exercise_index INTEGER NOT NULL,
|
||||
set_index INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
started_at INTEGER,
|
||||
accumulated_ms INTEGER NOT NULL,
|
||||
stopped_at INTEGER,
|
||||
skipped_at INTEGER,
|
||||
UNIQUE (active_workout_session_id, program_index, exercise_index, set_index),
|
||||
CHECK (length(trim(origin_device_id)) > 0),
|
||||
CHECK (future_owner_profile_id IS NULL OR length(trim(future_owner_profile_id)) > 0),
|
||||
CHECK (remote_revision IS NULL OR length(trim(remote_revision)) > 0),
|
||||
CHECK (program_index >= 0),
|
||||
CHECK (exercise_index >= 0),
|
||||
CHECK (set_index >= 0),
|
||||
CHECK (status IN ('running', 'paused', 'stopped', 'skipped')),
|
||||
CHECK (status != 'running' OR started_at IS NOT NULL),
|
||||
CHECK (accumulated_ms >= 0),
|
||||
CHECK (stopped_at IS NULL OR skipped_at IS NULL)
|
||||
)
|
||||
''');
|
||||
if (!await _hasColumn('active_rest_states', 'paused_at')) {
|
||||
await customStatement(
|
||||
'ALTER TABLE active_rest_states ADD COLUMN paused_at INTEGER',
|
||||
);
|
||||
}
|
||||
if (!await _hasColumn('active_rest_states', 'accumulated_paused_ms')) {
|
||||
await customStatement(
|
||||
'ALTER TABLE active_rest_states ADD COLUMN accumulated_paused_ms '
|
||||
'INTEGER NOT NULL DEFAULT 0 CHECK (accumulated_paused_ms >= 0)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _hasColumn(String tableName, String columnName) async {
|
||||
final rows = await customSelect('PRAGMA table_info($tableName)').get();
|
||||
return rows.any((row) => row.data['name'] == columnName);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -998,6 +998,17 @@ final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveSetTimerState(domain.ActiveSetTimerState state) async {
|
||||
await _upsertWithChangeLog(
|
||||
database: database,
|
||||
tableName: 'active_set_timer_states',
|
||||
entityType: 'ActiveSetTimerState',
|
||||
metadata: state.metadata,
|
||||
write: () => _upsertActiveSetTimerState(database, state),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveRestState(domain.ActiveRestState restState) async {
|
||||
await _upsertWithChangeLog(
|
||||
@ -1005,18 +1016,24 @@ final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
||||
tableName: 'active_rest_states',
|
||||
entityType: 'ActiveRestState',
|
||||
metadata: restState.metadata,
|
||||
write: () => database
|
||||
.into(database.activeRestStates)
|
||||
.insertOnConflictUpdate(_activeRestStateCompanion(restState)),
|
||||
write: () async {
|
||||
await database
|
||||
.into(database.activeRestStates)
|
||||
.insertOnConflictUpdate(_activeRestStateCompanion(restState));
|
||||
await _updateActiveRestPauseFields(database, restState);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.ActiveRestState?> findRestStateById(String id) async {
|
||||
final row = await (database.select(
|
||||
database.activeRestStates,
|
||||
)..where((table) => table.id.equals(id))).getSingleOrNull();
|
||||
return row == null ? null : _activeRestStateFromRow(row);
|
||||
final row = await database
|
||||
.customSelect(
|
||||
'SELECT * FROM active_rest_states WHERE id = ? LIMIT 1',
|
||||
variables: [Variable<String>(id)],
|
||||
)
|
||||
.getSingleOrNull();
|
||||
return row == null ? null : _activeRestStateFromCustomRow(row);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1122,6 +1139,33 @@ final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
||||
return row == null ? null : _activeScoreStopwatchStateFromRow(row);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.ActiveSetTimerState?> findSetTimerState({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) async {
|
||||
final row = await database
|
||||
.customSelect(
|
||||
'SELECT * FROM active_set_timer_states '
|
||||
'WHERE active_workout_session_id = ? '
|
||||
'AND program_index = ? '
|
||||
'AND exercise_index = ? '
|
||||
'AND set_index = ? '
|
||||
'AND deleted_at IS NULL '
|
||||
'LIMIT 1',
|
||||
variables: [
|
||||
Variable<String>(sessionId),
|
||||
Variable<int>(programIndex),
|
||||
Variable<int>(exerciseIndex),
|
||||
Variable<int>(setIndex),
|
||||
],
|
||||
)
|
||||
.getSingleOrNull();
|
||||
return row == null ? null : _activeSetTimerStateFromCustomRow(row);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<domain.ActiveExerciseStepProgressState?>
|
||||
findExerciseStepProgressState({
|
||||
@ -1158,6 +1202,22 @@ final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
||||
return rows.map(_activeSetResultFromRow).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<domain.ActiveSetTimerState>> listSetTimerStates(
|
||||
String sessionId,
|
||||
) async {
|
||||
final rows = await database
|
||||
.customSelect(
|
||||
'SELECT * FROM active_set_timer_states '
|
||||
'WHERE active_workout_session_id = ? '
|
||||
'AND deleted_at IS NULL '
|
||||
'ORDER BY program_index, exercise_index, set_index',
|
||||
variables: [Variable<String>(sessionId)],
|
||||
)
|
||||
.get();
|
||||
return rows.map(_activeSetTimerStateFromCustomRow).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<domain.ActiveExerciseStepProgressState>>
|
||||
listExerciseStepProgressStates(String sessionId) async {
|
||||
@ -1201,12 +1261,15 @@ final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
||||
|
||||
@override
|
||||
Future<List<domain.ActiveRestState>> listRestStates(String sessionId) async {
|
||||
final rows =
|
||||
await (database.select(database.activeRestStates)
|
||||
..where((table) => table.activeWorkoutSessionId.equals(sessionId))
|
||||
..orderBy([(table) => OrderingTerm.asc(table.startedAt)]))
|
||||
.get();
|
||||
return rows.map(_activeRestStateFromRow).toList();
|
||||
final rows = await database
|
||||
.customSelect(
|
||||
'SELECT * FROM active_rest_states '
|
||||
'WHERE active_workout_session_id = ? '
|
||||
'ORDER BY started_at',
|
||||
variables: [Variable<String>(sessionId)],
|
||||
)
|
||||
.get();
|
||||
return rows.map(_activeRestStateFromCustomRow).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1839,6 +1902,47 @@ domain.EntityMetadata _metadataFromRow(dynamic row) {
|
||||
);
|
||||
}
|
||||
|
||||
domain.EntityMetadata _metadataFromData(Map<String, dynamic> data) {
|
||||
return domain.EntityMetadata(
|
||||
id: data['id'] as String,
|
||||
createdAt: _dateTimeFromData(data, 'created_at'),
|
||||
updatedAt: _dateTimeFromData(data, 'updated_at'),
|
||||
deletedAt: _dateTimeOrNullFromData(data, 'deleted_at'),
|
||||
schemaVersion: data['schema_version'] as int,
|
||||
syncState: _syncStateFromDb(data['sync_state'] as String),
|
||||
localRevision: data['local_revision'] as int,
|
||||
originDeviceId: data['origin_device_id'] as String,
|
||||
futureOwnerProfileId: data['future_owner_profile_id'] as String?,
|
||||
lastSyncedAt: _dateTimeOrNullFromData(data, 'last_synced_at'),
|
||||
remoteRevision: data['remote_revision'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
DateTime _dateTimeFromData(Map<String, dynamic> data, String key) {
|
||||
final value = data[key];
|
||||
if (value is DateTime) {
|
||||
return value.toUtc();
|
||||
}
|
||||
if (value is int) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(value, isUtc: true);
|
||||
}
|
||||
throw domain.DomainException('Invalid timestamp column: $key');
|
||||
}
|
||||
|
||||
DateTime? _dateTimeOrNullFromData(Map<String, dynamic> data, String key) {
|
||||
final value = data[key];
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value is DateTime) {
|
||||
return value.toUtc();
|
||||
}
|
||||
if (value is int) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(value, isUtc: true);
|
||||
}
|
||||
throw domain.DomainException('Invalid timestamp column: $key');
|
||||
}
|
||||
|
||||
List<dynamic> _metadataValues(domain.EntityMetadata metadata) => [
|
||||
Value(metadata.id),
|
||||
Value(metadata.createdAt.toUtc()),
|
||||
@ -2458,18 +2562,112 @@ db.ActiveRestStatesCompanion _activeRestStateCompanion(
|
||||
);
|
||||
}
|
||||
|
||||
domain.ActiveRestState _activeRestStateFromRow(db.ActiveRestState row) {
|
||||
Future<void> _updateActiveRestPauseFields(
|
||||
db.AppDatabase database,
|
||||
domain.ActiveRestState restState,
|
||||
) async {
|
||||
await database.customUpdate(
|
||||
'UPDATE active_rest_states SET paused_at = ?, accumulated_paused_ms = ? '
|
||||
'WHERE id = ?',
|
||||
variables: [
|
||||
Variable<DateTime>(_utcOrNull(restState.pausedAt)),
|
||||
Variable<int>(restState.accumulatedPausedMs),
|
||||
Variable<String>(restState.metadata.id),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _upsertActiveSetTimerState(
|
||||
db.AppDatabase database,
|
||||
domain.ActiveSetTimerState state,
|
||||
) async {
|
||||
final metadata = state.metadata;
|
||||
await database.customInsert(
|
||||
'INSERT INTO active_set_timer_states ('
|
||||
'id, created_at, updated_at, deleted_at, schema_version, sync_state, '
|
||||
'local_revision, origin_device_id, future_owner_profile_id, '
|
||||
'last_synced_at, remote_revision, active_workout_session_id, '
|
||||
'program_index, exercise_index, set_index, status, started_at, '
|
||||
'accumulated_ms, stopped_at, skipped_at'
|
||||
') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) '
|
||||
'ON CONFLICT(active_workout_session_id, program_index, exercise_index, '
|
||||
'set_index) DO UPDATE SET '
|
||||
'id = excluded.id, '
|
||||
'created_at = excluded.created_at, '
|
||||
'updated_at = excluded.updated_at, '
|
||||
'deleted_at = excluded.deleted_at, '
|
||||
'schema_version = excluded.schema_version, '
|
||||
'sync_state = excluded.sync_state, '
|
||||
'local_revision = excluded.local_revision, '
|
||||
'origin_device_id = excluded.origin_device_id, '
|
||||
'future_owner_profile_id = excluded.future_owner_profile_id, '
|
||||
'last_synced_at = excluded.last_synced_at, '
|
||||
'remote_revision = excluded.remote_revision, '
|
||||
'active_workout_session_id = excluded.active_workout_session_id, '
|
||||
'program_index = excluded.program_index, '
|
||||
'exercise_index = excluded.exercise_index, '
|
||||
'set_index = excluded.set_index, '
|
||||
'status = excluded.status, '
|
||||
'started_at = excluded.started_at, '
|
||||
'accumulated_ms = excluded.accumulated_ms, '
|
||||
'stopped_at = excluded.stopped_at, '
|
||||
'skipped_at = excluded.skipped_at',
|
||||
variables: [
|
||||
Variable<String>(metadata.id),
|
||||
Variable<DateTime>(metadata.createdAt.toUtc()),
|
||||
Variable<DateTime>(metadata.updatedAt.toUtc()),
|
||||
Variable<DateTime>(_utcOrNull(metadata.deletedAt)),
|
||||
Variable<int>(metadata.schemaVersion),
|
||||
Variable<String>(_syncStateToDb(metadata.syncState)),
|
||||
Variable<int>(metadata.localRevision),
|
||||
Variable<String>(metadata.originDeviceId),
|
||||
Variable<String>(metadata.futureOwnerProfileId),
|
||||
Variable<DateTime>(_utcOrNull(metadata.lastSyncedAt)),
|
||||
Variable<String>(metadata.remoteRevision),
|
||||
Variable<String>(state.activeWorkoutSessionId),
|
||||
Variable<int>(state.programIndex),
|
||||
Variable<int>(state.exerciseIndex),
|
||||
Variable<int>(state.setIndex),
|
||||
Variable<String>(_setTimerStatusToDb(state.status)),
|
||||
Variable<DateTime>(_utcOrNull(state.startedAt)),
|
||||
Variable<int>(state.accumulatedMs),
|
||||
Variable<DateTime>(_utcOrNull(state.stoppedAt)),
|
||||
Variable<DateTime>(_utcOrNull(state.skippedAt)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
domain.ActiveSetTimerState _activeSetTimerStateFromCustomRow(QueryRow row) {
|
||||
final data = row.data;
|
||||
return domain.ActiveSetTimerState(
|
||||
metadata: _metadataFromData(data),
|
||||
activeWorkoutSessionId: data['active_workout_session_id'] as String,
|
||||
programIndex: data['program_index'] as int,
|
||||
exerciseIndex: data['exercise_index'] as int,
|
||||
setIndex: data['set_index'] as int,
|
||||
status: _setTimerStatusFromDb(data['status'] as String),
|
||||
startedAt: _dateTimeOrNullFromData(data, 'started_at'),
|
||||
accumulatedMs: data['accumulated_ms'] as int,
|
||||
stoppedAt: _dateTimeOrNullFromData(data, 'stopped_at'),
|
||||
skippedAt: _dateTimeOrNullFromData(data, 'skipped_at'),
|
||||
);
|
||||
}
|
||||
|
||||
domain.ActiveRestState _activeRestStateFromCustomRow(QueryRow row) {
|
||||
final data = row.data;
|
||||
return domain.ActiveRestState(
|
||||
metadata: _metadataFromRow(row),
|
||||
activeWorkoutSessionId: row.activeWorkoutSessionId,
|
||||
afterProgramIndex: row.afterProgramIndex,
|
||||
afterExerciseIndex: row.afterExerciseIndex,
|
||||
afterSetIndex: row.afterSetIndex,
|
||||
plannedRestSeconds: row.plannedRestSeconds,
|
||||
adjustedRestSeconds: row.adjustedRestSeconds,
|
||||
startedAt: _utc(row.startedAt),
|
||||
endedAt: _utcOrNull(row.endedAt),
|
||||
skippedAt: _utcOrNull(row.skippedAt),
|
||||
metadata: _metadataFromData(data),
|
||||
activeWorkoutSessionId: data['active_workout_session_id'] as String,
|
||||
afterProgramIndex: data['after_program_index'] as int,
|
||||
afterExerciseIndex: data['after_exercise_index'] as int,
|
||||
afterSetIndex: data['after_set_index'] as int,
|
||||
plannedRestSeconds: data['planned_rest_seconds'] as int,
|
||||
adjustedRestSeconds: data['adjusted_rest_seconds'] as int,
|
||||
startedAt: _dateTimeFromData(data, 'started_at'),
|
||||
endedAt: _dateTimeOrNullFromData(data, 'ended_at'),
|
||||
skippedAt: _dateTimeOrNullFromData(data, 'skipped_at'),
|
||||
pausedAt: _dateTimeOrNullFromData(data, 'paused_at'),
|
||||
accumulatedPausedMs: data['accumulated_paused_ms'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
@ -3509,18 +3707,39 @@ domain.SetResultStatus _setResultStatusFromDb(String value) => switch (value) {
|
||||
String _scoreStopwatchStatusToDb(domain.ActiveScoreStopwatchStatus status) =>
|
||||
switch (status) {
|
||||
domain.ActiveScoreStopwatchStatus.running => 'running',
|
||||
domain.ActiveScoreStopwatchStatus.paused => 'paused',
|
||||
domain.ActiveScoreStopwatchStatus.stopped => 'stopped',
|
||||
};
|
||||
|
||||
domain.ActiveScoreStopwatchStatus _scoreStopwatchStatusFromDb(String value) =>
|
||||
switch (value) {
|
||||
'running' => domain.ActiveScoreStopwatchStatus.running,
|
||||
'paused' => domain.ActiveScoreStopwatchStatus.paused,
|
||||
'stopped' => domain.ActiveScoreStopwatchStatus.stopped,
|
||||
_ => throw domain.DomainException(
|
||||
'Unknown active score stopwatch status: $value',
|
||||
),
|
||||
};
|
||||
|
||||
String _setTimerStatusToDb(domain.ActiveSetTimerStatus status) =>
|
||||
switch (status) {
|
||||
domain.ActiveSetTimerStatus.running => 'running',
|
||||
domain.ActiveSetTimerStatus.paused => 'paused',
|
||||
domain.ActiveSetTimerStatus.stopped => 'stopped',
|
||||
domain.ActiveSetTimerStatus.skipped => 'skipped',
|
||||
};
|
||||
|
||||
domain.ActiveSetTimerStatus _setTimerStatusFromDb(String value) =>
|
||||
switch (value) {
|
||||
'running' => domain.ActiveSetTimerStatus.running,
|
||||
'paused' => domain.ActiveSetTimerStatus.paused,
|
||||
'stopped' => domain.ActiveSetTimerStatus.stopped,
|
||||
'skipped' => domain.ActiveSetTimerStatus.skipped,
|
||||
_ => throw domain.DomainException(
|
||||
'Unknown active set timer status: $value',
|
||||
),
|
||||
};
|
||||
|
||||
String _exerciseStepProgressStatusToDb(
|
||||
domain.ActiveExerciseStepProgressStatus status,
|
||||
) => switch (status) {
|
||||
|
||||
@ -513,11 +513,40 @@ class ActiveScoreStopwatchStates extends SyncableTable {
|
||||
'CHECK (program_index >= 0)',
|
||||
'CHECK (exercise_index >= 0)',
|
||||
'CHECK (set_index >= 0)',
|
||||
"CHECK (status IN ('running', 'stopped'))",
|
||||
"CHECK (status IN ('running', 'paused', 'stopped'))",
|
||||
'CHECK (accumulated_ms >= 0)',
|
||||
];
|
||||
}
|
||||
|
||||
class ActiveSetTimerStates extends SyncableTable {
|
||||
@override
|
||||
String get tableName => 'active_set_timer_states';
|
||||
|
||||
TextColumn get activeWorkoutSessionId =>
|
||||
text().references(ActiveWorkoutSessions, #id)();
|
||||
IntColumn get programIndex => integer()();
|
||||
IntColumn get exerciseIndex => integer()();
|
||||
IntColumn get setIndex => integer()();
|
||||
TextColumn get status => text()();
|
||||
DateTimeColumn get startedAt => dateTime().nullable()();
|
||||
IntColumn get accumulatedMs => integer()();
|
||||
DateTimeColumn get stoppedAt => dateTime().nullable()();
|
||||
DateTimeColumn get skippedAt => dateTime().nullable()();
|
||||
|
||||
@override
|
||||
List<String> get customConstraints => [
|
||||
'UNIQUE (active_workout_session_id, program_index, exercise_index, '
|
||||
'set_index)',
|
||||
'CHECK (program_index >= 0)',
|
||||
'CHECK (exercise_index >= 0)',
|
||||
'CHECK (set_index >= 0)',
|
||||
"CHECK (status IN ('running', 'paused', 'stopped', 'skipped'))",
|
||||
'CHECK (status != \'running\' OR started_at IS NOT NULL)',
|
||||
'CHECK (accumulated_ms >= 0)',
|
||||
'CHECK (stopped_at IS NULL OR skipped_at IS NULL)',
|
||||
];
|
||||
}
|
||||
|
||||
class ActiveRestStates extends SyncableTable {
|
||||
@override
|
||||
String get tableName => 'active_rest_states';
|
||||
@ -532,6 +561,9 @@ class ActiveRestStates extends SyncableTable {
|
||||
DateTimeColumn get startedAt => dateTime()();
|
||||
DateTimeColumn get endedAt => dateTime().nullable()();
|
||||
DateTimeColumn get skippedAt => dateTime().nullable()();
|
||||
DateTimeColumn get pausedAt => dateTime().nullable()();
|
||||
IntColumn get accumulatedPausedMs =>
|
||||
integer().withDefault(const Constant(0))();
|
||||
|
||||
@override
|
||||
List<String> get customConstraints => [
|
||||
@ -540,6 +572,7 @@ class ActiveRestStates extends SyncableTable {
|
||||
'CHECK (after_set_index >= 0)',
|
||||
'CHECK (planned_rest_seconds >= 0)',
|
||||
'CHECK (adjusted_rest_seconds >= 0)',
|
||||
'CHECK (accumulated_paused_ms >= 0)',
|
||||
'CHECK (ended_at IS NULL OR skipped_at IS NULL)',
|
||||
];
|
||||
}
|
||||
|
||||
@ -405,7 +405,6 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
var _importingVideo = false;
|
||||
var _stepsEnabled = false;
|
||||
var _autoStartNextTimedStep = true;
|
||||
var _nextStepDraftId = 0;
|
||||
final _stepDrafts = <_ExerciseStepDraft>[];
|
||||
String? _measureError;
|
||||
String? _stepError;
|
||||
@ -1057,7 +1056,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
_stepError = null;
|
||||
_stepDrafts.add(
|
||||
_ExerciseStepDraft(
|
||||
id: 'step-draft-${_nextStepDraftId++}',
|
||||
id: widget.exerciseUseCases.ids.newId(),
|
||||
name: 'Nouvelle étape',
|
||||
defaultTargetValue: '10',
|
||||
),
|
||||
@ -1111,7 +1110,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
||||
_stepError = null;
|
||||
_stepDrafts.insert(
|
||||
index + 1,
|
||||
_stepDrafts[index].duplicate('step-draft-${_nextStepDraftId++}'),
|
||||
_stepDrafts[index].duplicate(widget.exerciseUseCases.ids.newId()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user