feat(domain): fondation domain du score chronométré (ticket #25)
Étend le modèle Drift (tables.dart, app_database.dart/.g.dart), les entités du domaine, les ports et use cases pour poser les fondations du score chronométré. build_runner OK, flutter analyze propre, 39/39 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -80,9 +80,26 @@ abstract interface class ActiveSessionRepository {
|
|||||||
Future<void> save(ActiveWorkoutSession session);
|
Future<void> save(ActiveWorkoutSession session);
|
||||||
Future<void> saveSetResult(ActiveSetResult result);
|
Future<void> saveSetResult(ActiveSetResult result);
|
||||||
Future<void> saveRestState(ActiveRestState restState);
|
Future<void> saveRestState(ActiveRestState restState);
|
||||||
|
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state);
|
||||||
|
Future<void> deleteScoreStopwatchState({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
required DateTime deletedAt,
|
||||||
|
});
|
||||||
Future<ActiveRestState?> findRestStateById(String id);
|
Future<ActiveRestState?> findRestStateById(String id);
|
||||||
|
Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
});
|
||||||
Future<List<ActiveSetResult>> listSetResults(String sessionId);
|
Future<List<ActiveSetResult>> listSetResults(String sessionId);
|
||||||
Future<List<ActiveRestState>> listRestStates(String sessionId);
|
Future<List<ActiveRestState>> listRestStates(String sessionId);
|
||||||
|
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||||||
|
String sessionId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract interface class WorkoutHistoryRepository {
|
abstract interface class WorkoutHistoryRepository {
|
||||||
|
|||||||
@ -24,6 +24,7 @@ final class ExerciseUseCases {
|
|||||||
required bool hasTimeMeasure,
|
required bool hasTimeMeasure,
|
||||||
required bool hasRepsMeasure,
|
required bool hasRepsMeasure,
|
||||||
required bool hasScoreMeasure,
|
required bool hasScoreMeasure,
|
||||||
|
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
|
||||||
String? scoreLabel,
|
String? scoreLabel,
|
||||||
String? scoreUnit,
|
String? scoreUnit,
|
||||||
}) async {
|
}) async {
|
||||||
@ -37,6 +38,7 @@ final class ExerciseUseCases {
|
|||||||
hasTimeMeasure: hasTimeMeasure,
|
hasTimeMeasure: hasTimeMeasure,
|
||||||
hasRepsMeasure: hasRepsMeasure,
|
hasRepsMeasure: hasRepsMeasure,
|
||||||
hasScoreMeasure: hasScoreMeasure,
|
hasScoreMeasure: hasScoreMeasure,
|
||||||
|
scoreInputMode: scoreInputMode,
|
||||||
scoreLabel: scoreLabel,
|
scoreLabel: scoreLabel,
|
||||||
scoreUnit: scoreUnit,
|
scoreUnit: scoreUnit,
|
||||||
);
|
);
|
||||||
@ -61,6 +63,7 @@ final class ExerciseUseCases {
|
|||||||
required bool hasTimeMeasure,
|
required bool hasTimeMeasure,
|
||||||
required bool hasRepsMeasure,
|
required bool hasRepsMeasure,
|
||||||
required bool hasScoreMeasure,
|
required bool hasScoreMeasure,
|
||||||
|
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
|
||||||
String? scoreLabel,
|
String? scoreLabel,
|
||||||
String? scoreUnit,
|
String? scoreUnit,
|
||||||
}) async {
|
}) async {
|
||||||
@ -77,6 +80,7 @@ final class ExerciseUseCases {
|
|||||||
hasTimeMeasure: hasTimeMeasure,
|
hasTimeMeasure: hasTimeMeasure,
|
||||||
hasRepsMeasure: hasRepsMeasure,
|
hasRepsMeasure: hasRepsMeasure,
|
||||||
hasScoreMeasure: hasScoreMeasure,
|
hasScoreMeasure: hasScoreMeasure,
|
||||||
|
scoreInputMode: scoreInputMode,
|
||||||
scoreLabel: scoreLabel,
|
scoreLabel: scoreLabel,
|
||||||
scoreUnit: scoreUnit,
|
scoreUnit: scoreUnit,
|
||||||
);
|
);
|
||||||
@ -285,6 +289,7 @@ final class ProgramUseCases {
|
|||||||
availableTimeSnapshot: input.availableTimeSnapshot,
|
availableTimeSnapshot: input.availableTimeSnapshot,
|
||||||
availableRepsSnapshot: input.availableRepsSnapshot,
|
availableRepsSnapshot: input.availableRepsSnapshot,
|
||||||
availableScoreSnapshot: input.availableScoreSnapshot,
|
availableScoreSnapshot: input.availableScoreSnapshot,
|
||||||
|
scoreInputModeSnapshot: input.scoreInputModeSnapshot,
|
||||||
scoreLabelSnapshot: input.scoreLabelSnapshot,
|
scoreLabelSnapshot: input.scoreLabelSnapshot,
|
||||||
scoreUnitSnapshot: input.scoreUnitSnapshot,
|
scoreUnitSnapshot: input.scoreUnitSnapshot,
|
||||||
setsCount: input.setsCount,
|
setsCount: input.setsCount,
|
||||||
@ -294,6 +299,7 @@ final class ProgramUseCases {
|
|||||||
targetTimeSeconds: input.targetTimeSeconds,
|
targetTimeSeconds: input.targetTimeSeconds,
|
||||||
targetReps: input.targetReps,
|
targetReps: input.targetReps,
|
||||||
targetScore: input.targetScore,
|
targetScore: input.targetScore,
|
||||||
|
targetScoreTimeMs: input.targetScoreTimeMs,
|
||||||
restSecondsOverride: input.restSecondsOverride,
|
restSecondsOverride: input.restSecondsOverride,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -317,6 +323,7 @@ final class ProgramUseCases {
|
|||||||
int? targetTimeSeconds,
|
int? targetTimeSeconds,
|
||||||
int? targetReps,
|
int? targetReps,
|
||||||
double? targetScore,
|
double? targetScore,
|
||||||
|
int? targetScoreTimeMs,
|
||||||
int? restSecondsOverride,
|
int? restSecondsOverride,
|
||||||
}) async {
|
}) async {
|
||||||
final source = await exerciseRepository.findById(exerciseId);
|
final source = await exerciseRepository.findById(exerciseId);
|
||||||
@ -334,6 +341,7 @@ final class ProgramUseCases {
|
|||||||
targetTimeSeconds: targetTimeSeconds,
|
targetTimeSeconds: targetTimeSeconds,
|
||||||
targetReps: targetReps,
|
targetReps: targetReps,
|
||||||
targetScore: targetScore,
|
targetScore: targetScore,
|
||||||
|
targetScoreTimeMs: targetScoreTimeMs,
|
||||||
restSecondsOverride: restSecondsOverride,
|
restSecondsOverride: restSecondsOverride,
|
||||||
);
|
);
|
||||||
await programRepository.saveExercise(programExercise);
|
await programRepository.saveExercise(programExercise);
|
||||||
@ -353,6 +361,7 @@ final class ProgramExerciseConfig {
|
|||||||
required this.availableTimeSnapshot,
|
required this.availableTimeSnapshot,
|
||||||
required this.availableRepsSnapshot,
|
required this.availableRepsSnapshot,
|
||||||
required this.availableScoreSnapshot,
|
required this.availableScoreSnapshot,
|
||||||
|
this.scoreInputModeSnapshot = ScoreInputMode.manual,
|
||||||
this.scoreLabelSnapshot,
|
this.scoreLabelSnapshot,
|
||||||
this.scoreUnitSnapshot,
|
this.scoreUnitSnapshot,
|
||||||
required this.setsCount,
|
required this.setsCount,
|
||||||
@ -360,6 +369,7 @@ final class ProgramExerciseConfig {
|
|||||||
this.targetTimeSeconds,
|
this.targetTimeSeconds,
|
||||||
this.targetReps,
|
this.targetReps,
|
||||||
this.targetScore,
|
this.targetScore,
|
||||||
|
this.targetScoreTimeMs,
|
||||||
this.restSecondsOverride,
|
this.restSecondsOverride,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -373,6 +383,7 @@ final class ProgramExerciseConfig {
|
|||||||
final bool availableTimeSnapshot;
|
final bool availableTimeSnapshot;
|
||||||
final bool availableRepsSnapshot;
|
final bool availableRepsSnapshot;
|
||||||
final bool availableScoreSnapshot;
|
final bool availableScoreSnapshot;
|
||||||
|
final ScoreInputMode scoreInputModeSnapshot;
|
||||||
final String? scoreLabelSnapshot;
|
final String? scoreLabelSnapshot;
|
||||||
final String? scoreUnitSnapshot;
|
final String? scoreUnitSnapshot;
|
||||||
final int setsCount;
|
final int setsCount;
|
||||||
@ -380,6 +391,7 @@ final class ProgramExerciseConfig {
|
|||||||
final int? targetTimeSeconds;
|
final int? targetTimeSeconds;
|
||||||
final int? targetReps;
|
final int? targetReps;
|
||||||
final double? targetScore;
|
final double? targetScore;
|
||||||
|
final int? targetScoreTimeMs;
|
||||||
final int? restSecondsOverride;
|
final int? restSecondsOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -480,6 +492,7 @@ final class WorkoutTemplateUseCases {
|
|||||||
targetTimeSecondsOverride: input.targetTimeSecondsOverride,
|
targetTimeSecondsOverride: input.targetTimeSecondsOverride,
|
||||||
targetRepsOverride: input.targetRepsOverride,
|
targetRepsOverride: input.targetRepsOverride,
|
||||||
targetScoreOverride: input.targetScoreOverride,
|
targetScoreOverride: input.targetScoreOverride,
|
||||||
|
targetScoreTimeMsOverride: input.targetScoreTimeMsOverride,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -521,6 +534,7 @@ final class WorkoutTemplateUseCases {
|
|||||||
int? targetTimeSecondsOverride,
|
int? targetTimeSecondsOverride,
|
||||||
int? targetRepsOverride,
|
int? targetRepsOverride,
|
||||||
double? targetScoreOverride,
|
double? targetScoreOverride,
|
||||||
|
int? targetScoreTimeMsOverride,
|
||||||
Set<WorkoutMeasure>? attemptedMeasureOverride,
|
Set<WorkoutMeasure>? attemptedMeasureOverride,
|
||||||
}) async {
|
}) async {
|
||||||
if (attemptedMeasureOverride != null) {
|
if (attemptedMeasureOverride != null) {
|
||||||
@ -537,6 +551,7 @@ final class WorkoutTemplateUseCases {
|
|||||||
targetTimeSecondsOverride: targetTimeSecondsOverride,
|
targetTimeSecondsOverride: targetTimeSecondsOverride,
|
||||||
targetRepsOverride: targetRepsOverride,
|
targetRepsOverride: targetRepsOverride,
|
||||||
targetScoreOverride: targetScoreOverride,
|
targetScoreOverride: targetScoreOverride,
|
||||||
|
targetScoreTimeMsOverride: targetScoreTimeMsOverride,
|
||||||
);
|
);
|
||||||
await templateRepository.saveOverride(override);
|
await templateRepository.saveOverride(override);
|
||||||
return override;
|
return override;
|
||||||
@ -570,6 +585,7 @@ final class WorkoutTemplateExerciseOverrideConfig {
|
|||||||
this.targetTimeSecondsOverride,
|
this.targetTimeSecondsOverride,
|
||||||
this.targetRepsOverride,
|
this.targetRepsOverride,
|
||||||
this.targetScoreOverride,
|
this.targetScoreOverride,
|
||||||
|
this.targetScoreTimeMsOverride,
|
||||||
});
|
});
|
||||||
|
|
||||||
final EntityMetadata? existingMetadata;
|
final EntityMetadata? existingMetadata;
|
||||||
@ -579,6 +595,7 @@ final class WorkoutTemplateExerciseOverrideConfig {
|
|||||||
final int? targetTimeSecondsOverride;
|
final int? targetTimeSecondsOverride;
|
||||||
final int? targetRepsOverride;
|
final int? targetRepsOverride;
|
||||||
final double? targetScoreOverride;
|
final double? targetScoreOverride;
|
||||||
|
final int? targetScoreTimeMsOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ActiveWorkoutSessionUseCases {
|
final class ActiveWorkoutSessionUseCases {
|
||||||
@ -635,6 +652,7 @@ final class ActiveWorkoutSessionUseCases {
|
|||||||
'targetTimeSecondsOverride': override.targetTimeSecondsOverride,
|
'targetTimeSecondsOverride': override.targetTimeSecondsOverride,
|
||||||
'targetRepsOverride': override.targetRepsOverride,
|
'targetRepsOverride': override.targetRepsOverride,
|
||||||
'targetScoreOverride': override.targetScoreOverride,
|
'targetScoreOverride': override.targetScoreOverride,
|
||||||
|
'targetScoreTimeMsOverride': override.targetScoreTimeMsOverride,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -668,7 +686,21 @@ final class ActiveWorkoutSessionUseCases {
|
|||||||
|
|
||||||
Future<ActiveWorkoutSession> pause(String sessionId) async {
|
Future<ActiveWorkoutSession> pause(String sessionId) async {
|
||||||
final session = await _requiredSession(sessionId);
|
final session = await _requiredSession(sessionId);
|
||||||
final paused = session.pause(clock.now());
|
final now = clock.now();
|
||||||
|
final runningStopwatches =
|
||||||
|
(await sessionRepository.listScoreStopwatchStates(
|
||||||
|
sessionId,
|
||||||
|
)).where((state) => state.status == ActiveScoreStopwatchStatus.running);
|
||||||
|
for (final state in runningStopwatches) {
|
||||||
|
final stopped = state.copyWith(
|
||||||
|
metadata: state.metadata.touch(now),
|
||||||
|
status: ActiveScoreStopwatchStatus.stopped,
|
||||||
|
accumulatedMs: state.elapsedMillisecondsAt(now),
|
||||||
|
stoppedAt: now,
|
||||||
|
);
|
||||||
|
await sessionRepository.saveScoreStopwatchState(stopped);
|
||||||
|
}
|
||||||
|
final paused = session.pause(now);
|
||||||
await sessionRepository.save(paused);
|
await sessionRepository.save(paused);
|
||||||
return paused;
|
return paused;
|
||||||
}
|
}
|
||||||
@ -699,10 +731,21 @@ final class ActiveWorkoutSessionUseCases {
|
|||||||
int? actualTimeMs,
|
int? actualTimeMs,
|
||||||
int? actualReps,
|
int? actualReps,
|
||||||
double? actualScore,
|
double? actualScore,
|
||||||
|
int? actualScoreTimeMs,
|
||||||
|
ScoreInputMode scoreInputModeSnapshot = ScoreInputMode.manual,
|
||||||
String? scoreLabelSnapshot,
|
String? scoreLabelSnapshot,
|
||||||
String? scoreUnitSnapshot,
|
String? scoreUnitSnapshot,
|
||||||
}) async {
|
}) async {
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
|
final resolvedActualScoreTimeMs = await _resolveStopwatchScoreTimeMs(
|
||||||
|
sessionId: sessionId,
|
||||||
|
programIndex: programIndex,
|
||||||
|
exerciseIndex: exerciseIndex,
|
||||||
|
setIndex: setIndex,
|
||||||
|
scoreInputMode: scoreInputModeSnapshot,
|
||||||
|
explicitActualScoreTimeMs: actualScoreTimeMs,
|
||||||
|
now: now,
|
||||||
|
);
|
||||||
final result = ActiveSetResult(
|
final result = ActiveSetResult(
|
||||||
metadata: _newMetadata(ids, originDeviceId, now),
|
metadata: _newMetadata(ids, originDeviceId, now),
|
||||||
activeWorkoutSessionId: sessionId,
|
activeWorkoutSessionId: sessionId,
|
||||||
@ -715,6 +758,8 @@ final class ActiveWorkoutSessionUseCases {
|
|||||||
actualTimeMs: actualTimeMs,
|
actualTimeMs: actualTimeMs,
|
||||||
actualReps: actualReps,
|
actualReps: actualReps,
|
||||||
actualScore: actualScore,
|
actualScore: actualScore,
|
||||||
|
actualScoreTimeMs: resolvedActualScoreTimeMs,
|
||||||
|
scoreInputModeSnapshot: scoreInputModeSnapshot,
|
||||||
scoreLabelSnapshot: scoreLabelSnapshot,
|
scoreLabelSnapshot: scoreLabelSnapshot,
|
||||||
scoreUnitSnapshot: scoreUnitSnapshot,
|
scoreUnitSnapshot: scoreUnitSnapshot,
|
||||||
status: SetResultStatus.completed,
|
status: SetResultStatus.completed,
|
||||||
@ -732,6 +777,8 @@ final class ActiveWorkoutSessionUseCases {
|
|||||||
int? actualTimeMs,
|
int? actualTimeMs,
|
||||||
int? actualReps,
|
int? actualReps,
|
||||||
double? actualScore,
|
double? actualScore,
|
||||||
|
int? actualScoreTimeMs,
|
||||||
|
ScoreInputMode? scoreInputModeSnapshot,
|
||||||
String? scoreLabelSnapshot,
|
String? scoreLabelSnapshot,
|
||||||
String? scoreUnitSnapshot,
|
String? scoreUnitSnapshot,
|
||||||
String? note,
|
String? note,
|
||||||
@ -766,6 +813,8 @@ final class ActiveWorkoutSessionUseCases {
|
|||||||
}
|
}
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
final isSkipped = status == SetResultStatus.skipped;
|
final isSkipped = status == SetResultStatus.skipped;
|
||||||
|
final resolvedScoreInputMode =
|
||||||
|
scoreInputModeSnapshot ?? snapshot.scoreInputModeSnapshot;
|
||||||
final upserted = ActiveSetResult(
|
final upserted = ActiveSetResult(
|
||||||
metadata: existing == null
|
metadata: existing == null
|
||||||
? _newMetadata(ids, originDeviceId, now)
|
? _newMetadata(ids, originDeviceId, now)
|
||||||
@ -781,6 +830,8 @@ final class ActiveWorkoutSessionUseCases {
|
|||||||
actualTimeMs: isSkipped ? null : actualTimeMs,
|
actualTimeMs: isSkipped ? null : actualTimeMs,
|
||||||
actualReps: isSkipped ? null : actualReps,
|
actualReps: isSkipped ? null : actualReps,
|
||||||
actualScore: isSkipped ? null : actualScore,
|
actualScore: isSkipped ? null : actualScore,
|
||||||
|
actualScoreTimeMs: isSkipped ? null : actualScoreTimeMs,
|
||||||
|
scoreInputModeSnapshot: resolvedScoreInputMode,
|
||||||
scoreLabelSnapshot: isSkipped
|
scoreLabelSnapshot: isSkipped
|
||||||
? null
|
? null
|
||||||
: scoreLabelSnapshot ?? snapshot.scoreLabelSnapshot,
|
: scoreLabelSnapshot ?? snapshot.scoreLabelSnapshot,
|
||||||
@ -794,6 +845,102 @@ final class ActiveWorkoutSessionUseCases {
|
|||||||
return upserted;
|
return upserted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ActiveScoreStopwatchState> startScoreStopwatch({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
}) async {
|
||||||
|
await _requiredSession(sessionId);
|
||||||
|
final now = clock.now();
|
||||||
|
final existing = await sessionRepository.findScoreStopwatchState(
|
||||||
|
sessionId: sessionId,
|
||||||
|
programIndex: programIndex,
|
||||||
|
exerciseIndex: exerciseIndex,
|
||||||
|
setIndex: setIndex,
|
||||||
|
);
|
||||||
|
if (existing != null) {
|
||||||
|
if (existing.status == ActiveScoreStopwatchStatus.running) {
|
||||||
|
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<ActiveScoreStopwatchState> stopScoreStopwatch({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
}) async {
|
||||||
|
final state = await _requiredStopwatchState(
|
||||||
|
sessionId: sessionId,
|
||||||
|
programIndex: programIndex,
|
||||||
|
exerciseIndex: exerciseIndex,
|
||||||
|
setIndex: setIndex,
|
||||||
|
);
|
||||||
|
if (state.status == ActiveScoreStopwatchStatus.stopped) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
final now = clock.now();
|
||||||
|
final stopped = state.copyWith(
|
||||||
|
metadata: state.metadata.touch(now),
|
||||||
|
status: ActiveScoreStopwatchStatus.stopped,
|
||||||
|
accumulatedMs: state.elapsedMillisecondsAt(now),
|
||||||
|
stoppedAt: now,
|
||||||
|
);
|
||||||
|
await sessionRepository.saveScoreStopwatchState(stopped);
|
||||||
|
return stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ActiveScoreStopwatchState> resumeScoreStopwatch({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
}) {
|
||||||
|
return startScoreStopwatch(
|
||||||
|
sessionId: sessionId,
|
||||||
|
programIndex: programIndex,
|
||||||
|
exerciseIndex: exerciseIndex,
|
||||||
|
setIndex: setIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> resetScoreStopwatch({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
}) async {
|
||||||
|
await sessionRepository.deleteScoreStopwatchState(
|
||||||
|
sessionId: sessionId,
|
||||||
|
programIndex: programIndex,
|
||||||
|
exerciseIndex: exerciseIndex,
|
||||||
|
setIndex: setIndex,
|
||||||
|
deletedAt: clock.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<SetResultPositionState>> listSetResults(String sessionId) async {
|
Future<List<SetResultPositionState>> listSetResults(String sessionId) async {
|
||||||
final session = await _requiredSession(sessionId);
|
final session = await _requiredSession(sessionId);
|
||||||
final snapshots = _listSetSnapshots(session.resolvedTemplateSnapshotJson);
|
final snapshots = _listSetSnapshots(session.resolvedTemplateSnapshotJson);
|
||||||
@ -960,6 +1107,58 @@ final class ActiveWorkoutSessionUseCases {
|
|||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ActiveScoreStopwatchState> _requiredStopwatchState({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
}) async {
|
||||||
|
final state = await sessionRepository.findScoreStopwatchState(
|
||||||
|
sessionId: sessionId,
|
||||||
|
programIndex: programIndex,
|
||||||
|
exerciseIndex: exerciseIndex,
|
||||||
|
setIndex: setIndex,
|
||||||
|
);
|
||||||
|
if (state == null) {
|
||||||
|
throw const DomainException('Active score stopwatch state not found.');
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int?> _resolveStopwatchScoreTimeMs({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
required ScoreInputMode scoreInputMode,
|
||||||
|
required int? explicitActualScoreTimeMs,
|
||||||
|
required DateTime now,
|
||||||
|
}) async {
|
||||||
|
if (scoreInputMode != ScoreInputMode.stopwatch) {
|
||||||
|
return explicitActualScoreTimeMs;
|
||||||
|
}
|
||||||
|
final state = await sessionRepository.findScoreStopwatchState(
|
||||||
|
sessionId: sessionId,
|
||||||
|
programIndex: programIndex,
|
||||||
|
exerciseIndex: exerciseIndex,
|
||||||
|
setIndex: setIndex,
|
||||||
|
);
|
||||||
|
if (state == null) {
|
||||||
|
return explicitActualScoreTimeMs;
|
||||||
|
}
|
||||||
|
if (state.status == ActiveScoreStopwatchStatus.stopped) {
|
||||||
|
return explicitActualScoreTimeMs ?? state.accumulatedMs;
|
||||||
|
}
|
||||||
|
final stopped = state.copyWith(
|
||||||
|
metadata: state.metadata.touch(now),
|
||||||
|
status: ActiveScoreStopwatchStatus.stopped,
|
||||||
|
accumulatedMs: state.elapsedMillisecondsAt(now),
|
||||||
|
stoppedAt: now,
|
||||||
|
);
|
||||||
|
await sessionRepository.saveScoreStopwatchState(stopped);
|
||||||
|
return explicitActualScoreTimeMs ?? stopped.accumulatedMs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class CloseWorkoutSessionUseCase {
|
final class CloseWorkoutSessionUseCase {
|
||||||
@ -1025,6 +1224,8 @@ final class CloseWorkoutSessionUseCase {
|
|||||||
'actualTimeMs': result.actualTimeMs,
|
'actualTimeMs': result.actualTimeMs,
|
||||||
'actualReps': result.actualReps,
|
'actualReps': result.actualReps,
|
||||||
'actualScore': result.actualScore,
|
'actualScore': result.actualScore,
|
||||||
|
'actualScoreTimeMs': result.actualScoreTimeMs,
|
||||||
|
'scoreInputModeSnapshot': result.scoreInputModeSnapshot.name,
|
||||||
'scoreLabelSnapshot': result.scoreLabelSnapshot,
|
'scoreLabelSnapshot': result.scoreLabelSnapshot,
|
||||||
'scoreUnitSnapshot': result.scoreUnitSnapshot,
|
'scoreUnitSnapshot': result.scoreUnitSnapshot,
|
||||||
'status': result.status.name,
|
'status': result.status.name,
|
||||||
@ -1096,6 +1297,22 @@ void _validateOverrideTargets(
|
|||||||
if (input.targetScoreOverride != null && exercise['scoreEnabled'] != true) {
|
if (input.targetScoreOverride != null && exercise['scoreEnabled'] != true) {
|
||||||
throw const DomainException('Cannot override inactive score target.');
|
throw const DomainException('Cannot override inactive score target.');
|
||||||
}
|
}
|
||||||
|
if (input.targetScoreTimeMsOverride != null &&
|
||||||
|
exercise['scoreEnabled'] != true) {
|
||||||
|
throw const DomainException('Cannot override inactive score target.');
|
||||||
|
}
|
||||||
|
final mode = _scoreInputModeFromSnapshot(exercise['scoreInputModeSnapshot']);
|
||||||
|
if (input.targetScoreOverride != null && mode != ScoreInputMode.manual) {
|
||||||
|
throw const DomainException(
|
||||||
|
'Cannot override manual score target on stopwatch score mode.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (input.targetScoreTimeMsOverride != null &&
|
||||||
|
mode != ScoreInputMode.stopwatch) {
|
||||||
|
throw const DomainException(
|
||||||
|
'Cannot override stopwatch score target on manual score mode.',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _ensureEditablePastPosition({
|
void _ensureEditablePastPosition({
|
||||||
@ -1142,6 +1359,13 @@ String _positionKey(int programIndex, int exerciseIndex, int setIndex) {
|
|||||||
return '$programIndex:$exerciseIndex:$setIndex';
|
return '$programIndex:$exerciseIndex:$setIndex';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScoreInputMode _scoreInputModeFromSnapshot(Object? value) {
|
||||||
|
return switch (value) {
|
||||||
|
'stopwatch' => ScoreInputMode.stopwatch,
|
||||||
|
_ => ScoreInputMode.manual,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
_SetPositionSnapshot? _findSetSnapshot({
|
_SetPositionSnapshot? _findSetSnapshot({
|
||||||
required String resolvedTemplateSnapshotJson,
|
required String resolvedTemplateSnapshotJson,
|
||||||
required int programIndex,
|
required int programIndex,
|
||||||
@ -1196,6 +1420,9 @@ List<_SetPositionSnapshot> _listSetSnapshots(
|
|||||||
setIndex: setIndex,
|
setIndex: setIndex,
|
||||||
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
||||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||||
|
scoreInputModeSnapshot: _scoreInputModeFromSnapshot(
|
||||||
|
exercise['scoreInputModeSnapshot'],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1213,6 +1440,7 @@ final class _SetPositionSnapshot {
|
|||||||
required this.setIndex,
|
required this.setIndex,
|
||||||
this.scoreLabelSnapshot,
|
this.scoreLabelSnapshot,
|
||||||
this.scoreUnitSnapshot,
|
this.scoreUnitSnapshot,
|
||||||
|
required this.scoreInputModeSnapshot,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String programSnapshotId;
|
final String programSnapshotId;
|
||||||
@ -1222,6 +1450,7 @@ final class _SetPositionSnapshot {
|
|||||||
final int setIndex;
|
final int setIndex;
|
||||||
final String? scoreLabelSnapshot;
|
final String? scoreLabelSnapshot;
|
||||||
final String? scoreUnitSnapshot;
|
final String? scoreUnitSnapshot;
|
||||||
|
final ScoreInputMode scoreInputModeSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<WorkoutHistorySetResult> _historyResultsFromActiveResults({
|
List<WorkoutHistorySetResult> _historyResultsFromActiveResults({
|
||||||
@ -1250,13 +1479,18 @@ List<WorkoutHistorySetResult> _historyResultsFromActiveResults({
|
|||||||
timeEnabledSnapshot: snapshot?.timeEnabled ?? result.actualTimeMs != null,
|
timeEnabledSnapshot: snapshot?.timeEnabled ?? result.actualTimeMs != null,
|
||||||
repsEnabledSnapshot: snapshot?.repsEnabled ?? result.actualReps != null,
|
repsEnabledSnapshot: snapshot?.repsEnabled ?? result.actualReps != null,
|
||||||
scoreEnabledSnapshot:
|
scoreEnabledSnapshot:
|
||||||
snapshot?.scoreEnabled ?? result.actualScore != null,
|
snapshot?.scoreEnabled ??
|
||||||
|
(result.actualScore != null || result.actualScoreTimeMs != null),
|
||||||
targetTimeSecondsSnapshot: snapshot?.targetTimeSeconds,
|
targetTimeSecondsSnapshot: snapshot?.targetTimeSeconds,
|
||||||
targetRepsSnapshot: snapshot?.targetReps,
|
targetRepsSnapshot: snapshot?.targetReps,
|
||||||
targetScoreSnapshot: snapshot?.targetScore,
|
targetScoreSnapshot: snapshot?.targetScore,
|
||||||
|
targetScoreTimeMsSnapshot: snapshot?.targetScoreTimeMs,
|
||||||
actualTimeMs: result.actualTimeMs,
|
actualTimeMs: result.actualTimeMs,
|
||||||
actualReps: result.actualReps,
|
actualReps: result.actualReps,
|
||||||
actualScore: result.actualScore,
|
actualScore: result.actualScore,
|
||||||
|
actualScoreTimeMs: result.actualScoreTimeMs,
|
||||||
|
scoreInputModeSnapshot:
|
||||||
|
snapshot?.scoreInputModeSnapshot ?? result.scoreInputModeSnapshot,
|
||||||
scoreLabelSnapshot:
|
scoreLabelSnapshot:
|
||||||
result.scoreLabelSnapshot ?? snapshot?.scoreLabelSnapshot,
|
result.scoreLabelSnapshot ?? snapshot?.scoreLabelSnapshot,
|
||||||
scoreUnitSnapshot:
|
scoreUnitSnapshot:
|
||||||
@ -1299,6 +1533,10 @@ Map<String, _ResolvedExerciseSnapshot> _exerciseSnapshotsById(
|
|||||||
targetTimeSeconds: exercise['targetTimeSeconds'] as int?,
|
targetTimeSeconds: exercise['targetTimeSeconds'] as int?,
|
||||||
targetReps: exercise['targetReps'] as int?,
|
targetReps: exercise['targetReps'] as int?,
|
||||||
targetScore: (exercise['targetScore'] as num?)?.toDouble(),
|
targetScore: (exercise['targetScore'] as num?)?.toDouble(),
|
||||||
|
targetScoreTimeMs: exercise['targetScoreTimeMs'] as int?,
|
||||||
|
scoreInputModeSnapshot: _scoreInputModeFromSnapshot(
|
||||||
|
exercise['scoreInputModeSnapshot'],
|
||||||
|
),
|
||||||
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
||||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||||
);
|
);
|
||||||
@ -1317,6 +1555,8 @@ final class _ResolvedExerciseSnapshot {
|
|||||||
this.targetTimeSeconds,
|
this.targetTimeSeconds,
|
||||||
this.targetReps,
|
this.targetReps,
|
||||||
this.targetScore,
|
this.targetScore,
|
||||||
|
this.targetScoreTimeMs,
|
||||||
|
required this.scoreInputModeSnapshot,
|
||||||
this.scoreLabelSnapshot,
|
this.scoreLabelSnapshot,
|
||||||
this.scoreUnitSnapshot,
|
this.scoreUnitSnapshot,
|
||||||
});
|
});
|
||||||
@ -1329,6 +1569,8 @@ final class _ResolvedExerciseSnapshot {
|
|||||||
final int? targetTimeSeconds;
|
final int? targetTimeSeconds;
|
||||||
final int? targetReps;
|
final int? targetReps;
|
||||||
final double? targetScore;
|
final double? targetScore;
|
||||||
|
final int? targetScoreTimeMs;
|
||||||
|
final ScoreInputMode scoreInputModeSnapshot;
|
||||||
final String? scoreLabelSnapshot;
|
final String? scoreLabelSnapshot;
|
||||||
final String? scoreUnitSnapshot;
|
final String? scoreUnitSnapshot;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,10 +6,14 @@ enum MediaKind { image, video }
|
|||||||
|
|
||||||
enum WorkoutMeasure { time, reps, score }
|
enum WorkoutMeasure { time, reps, score }
|
||||||
|
|
||||||
|
enum ScoreInputMode { manual, stopwatch }
|
||||||
|
|
||||||
enum ActiveWorkoutStatus { running, paused, savedExit, completed, abandoned }
|
enum ActiveWorkoutStatus { running, paused, savedExit, completed, abandoned }
|
||||||
|
|
||||||
enum SetResultStatus { completed, skipped }
|
enum SetResultStatus { completed, skipped }
|
||||||
|
|
||||||
|
enum ActiveScoreStopwatchStatus { running, stopped }
|
||||||
|
|
||||||
final class DomainException implements Exception {
|
final class DomainException implements Exception {
|
||||||
const DomainException(this.message);
|
const DomainException(this.message);
|
||||||
|
|
||||||
@ -125,6 +129,7 @@ final class Exercise {
|
|||||||
required this.hasTimeMeasure,
|
required this.hasTimeMeasure,
|
||||||
required this.hasRepsMeasure,
|
required this.hasRepsMeasure,
|
||||||
required this.hasScoreMeasure,
|
required this.hasScoreMeasure,
|
||||||
|
this.scoreInputMode = ScoreInputMode.manual,
|
||||||
this.scoreLabel,
|
this.scoreLabel,
|
||||||
this.scoreUnit,
|
this.scoreUnit,
|
||||||
this.archivedAt,
|
this.archivedAt,
|
||||||
@ -134,7 +139,12 @@ final class Exercise {
|
|||||||
hasReps: hasRepsMeasure,
|
hasReps: hasRepsMeasure,
|
||||||
hasScore: hasScoreMeasure,
|
hasScore: hasScoreMeasure,
|
||||||
);
|
);
|
||||||
if (hasScoreMeasure) {
|
if (!hasScoreMeasure && scoreInputMode == ScoreInputMode.stopwatch) {
|
||||||
|
throw const DomainException(
|
||||||
|
'Stopwatch score mode requires an active score measure.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (hasScoreMeasure && scoreInputMode == ScoreInputMode.manual) {
|
||||||
_nonBlank(scoreLabel, 'Score label');
|
_nonBlank(scoreLabel, 'Score label');
|
||||||
_nonBlank(scoreUnit, 'Score unit');
|
_nonBlank(scoreUnit, 'Score unit');
|
||||||
}
|
}
|
||||||
@ -148,6 +158,7 @@ final class Exercise {
|
|||||||
final bool hasTimeMeasure;
|
final bool hasTimeMeasure;
|
||||||
final bool hasRepsMeasure;
|
final bool hasRepsMeasure;
|
||||||
final bool hasScoreMeasure;
|
final bool hasScoreMeasure;
|
||||||
|
final ScoreInputMode scoreInputMode;
|
||||||
final String? scoreLabel;
|
final String? scoreLabel;
|
||||||
final String? scoreUnit;
|
final String? scoreUnit;
|
||||||
final DateTime? archivedAt;
|
final DateTime? archivedAt;
|
||||||
@ -171,6 +182,7 @@ final class Exercise {
|
|||||||
bool? hasTimeMeasure,
|
bool? hasTimeMeasure,
|
||||||
bool? hasRepsMeasure,
|
bool? hasRepsMeasure,
|
||||||
bool? hasScoreMeasure,
|
bool? hasScoreMeasure,
|
||||||
|
ScoreInputMode? scoreInputMode,
|
||||||
Object? scoreLabel = _unchanged,
|
Object? scoreLabel = _unchanged,
|
||||||
Object? scoreUnit = _unchanged,
|
Object? scoreUnit = _unchanged,
|
||||||
Object? archivedAt = _unchanged,
|
Object? archivedAt = _unchanged,
|
||||||
@ -190,6 +202,7 @@ final class Exercise {
|
|||||||
hasTimeMeasure: hasTimeMeasure ?? this.hasTimeMeasure,
|
hasTimeMeasure: hasTimeMeasure ?? this.hasTimeMeasure,
|
||||||
hasRepsMeasure: hasRepsMeasure ?? this.hasRepsMeasure,
|
hasRepsMeasure: hasRepsMeasure ?? this.hasRepsMeasure,
|
||||||
hasScoreMeasure: hasScoreMeasure ?? this.hasScoreMeasure,
|
hasScoreMeasure: hasScoreMeasure ?? this.hasScoreMeasure,
|
||||||
|
scoreInputMode: scoreInputMode ?? this.scoreInputMode,
|
||||||
scoreLabel: scoreLabel == _unchanged
|
scoreLabel: scoreLabel == _unchanged
|
||||||
? this.scoreLabel
|
? this.scoreLabel
|
||||||
: scoreLabel as String?,
|
: scoreLabel as String?,
|
||||||
@ -249,6 +262,7 @@ final class ProgramExercise {
|
|||||||
required this.availableTimeSnapshot,
|
required this.availableTimeSnapshot,
|
||||||
required this.availableRepsSnapshot,
|
required this.availableRepsSnapshot,
|
||||||
required this.availableScoreSnapshot,
|
required this.availableScoreSnapshot,
|
||||||
|
this.scoreInputModeSnapshot = ScoreInputMode.manual,
|
||||||
this.scoreLabelSnapshot,
|
this.scoreLabelSnapshot,
|
||||||
this.scoreUnitSnapshot,
|
this.scoreUnitSnapshot,
|
||||||
required this.setsCount,
|
required this.setsCount,
|
||||||
@ -258,6 +272,7 @@ final class ProgramExercise {
|
|||||||
this.targetTimeSeconds,
|
this.targetTimeSeconds,
|
||||||
this.targetReps,
|
this.targetReps,
|
||||||
this.targetScore,
|
this.targetScore,
|
||||||
|
this.targetScoreTimeMs,
|
||||||
this.restSecondsOverride,
|
this.restSecondsOverride,
|
||||||
}) : exerciseNameSnapshot = _nonBlank(
|
}) : exerciseNameSnapshot = _nonBlank(
|
||||||
exerciseNameSnapshot,
|
exerciseNameSnapshot,
|
||||||
@ -282,7 +297,14 @@ final class ProgramExercise {
|
|||||||
_requireNullablePositive(targetTimeSeconds, 'Target time seconds');
|
_requireNullablePositive(targetTimeSeconds, 'Target time seconds');
|
||||||
_requireNullablePositive(targetReps, 'Target reps');
|
_requireNullablePositive(targetReps, 'Target reps');
|
||||||
_requireNullableNonNegativeDouble(targetScore, 'Target score');
|
_requireNullableNonNegativeDouble(targetScore, 'Target score');
|
||||||
|
_requireNullablePositive(targetScoreTimeMs, 'Target score time ms');
|
||||||
_requireNullableNonNegative(restSecondsOverride, 'Rest seconds override');
|
_requireNullableNonNegative(restSecondsOverride, 'Rest seconds override');
|
||||||
|
_requireScoreTargetShape(
|
||||||
|
scoreEnabled: scoreEnabled,
|
||||||
|
scoreInputMode: scoreInputModeSnapshot,
|
||||||
|
targetScore: targetScore,
|
||||||
|
targetScoreTimeMs: targetScoreTimeMs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final EntityMetadata metadata;
|
final EntityMetadata metadata;
|
||||||
@ -297,6 +319,7 @@ final class ProgramExercise {
|
|||||||
final bool availableTimeSnapshot;
|
final bool availableTimeSnapshot;
|
||||||
final bool availableRepsSnapshot;
|
final bool availableRepsSnapshot;
|
||||||
final bool availableScoreSnapshot;
|
final bool availableScoreSnapshot;
|
||||||
|
final ScoreInputMode scoreInputModeSnapshot;
|
||||||
final String? scoreLabelSnapshot;
|
final String? scoreLabelSnapshot;
|
||||||
final String? scoreUnitSnapshot;
|
final String? scoreUnitSnapshot;
|
||||||
final int setsCount;
|
final int setsCount;
|
||||||
@ -306,6 +329,7 @@ final class ProgramExercise {
|
|||||||
final int? targetTimeSeconds;
|
final int? targetTimeSeconds;
|
||||||
final int? targetReps;
|
final int? targetReps;
|
||||||
final double? targetScore;
|
final double? targetScore;
|
||||||
|
final int? targetScoreTimeMs;
|
||||||
final int? restSecondsOverride;
|
final int? restSecondsOverride;
|
||||||
|
|
||||||
static ProgramExercise snapshotFromExercise({
|
static ProgramExercise snapshotFromExercise({
|
||||||
@ -318,6 +342,7 @@ final class ProgramExercise {
|
|||||||
int? targetTimeSeconds,
|
int? targetTimeSeconds,
|
||||||
int? targetReps,
|
int? targetReps,
|
||||||
double? targetScore,
|
double? targetScore,
|
||||||
|
int? targetScoreTimeMs,
|
||||||
int? restSecondsOverride,
|
int? restSecondsOverride,
|
||||||
}) {
|
}) {
|
||||||
final available = exercise.availableMeasures;
|
final available = exercise.availableMeasures;
|
||||||
@ -339,6 +364,7 @@ final class ProgramExercise {
|
|||||||
availableTimeSnapshot: exercise.hasTimeMeasure,
|
availableTimeSnapshot: exercise.hasTimeMeasure,
|
||||||
availableRepsSnapshot: exercise.hasRepsMeasure,
|
availableRepsSnapshot: exercise.hasRepsMeasure,
|
||||||
availableScoreSnapshot: exercise.hasScoreMeasure,
|
availableScoreSnapshot: exercise.hasScoreMeasure,
|
||||||
|
scoreInputModeSnapshot: exercise.scoreInputMode,
|
||||||
scoreLabelSnapshot: exercise.scoreLabel,
|
scoreLabelSnapshot: exercise.scoreLabel,
|
||||||
scoreUnitSnapshot: exercise.scoreUnit,
|
scoreUnitSnapshot: exercise.scoreUnit,
|
||||||
setsCount: setsCount,
|
setsCount: setsCount,
|
||||||
@ -348,6 +374,7 @@ final class ProgramExercise {
|
|||||||
targetTimeSeconds: targetTimeSeconds,
|
targetTimeSeconds: targetTimeSeconds,
|
||||||
targetReps: targetReps,
|
targetReps: targetReps,
|
||||||
targetScore: targetScore,
|
targetScore: targetScore,
|
||||||
|
targetScoreTimeMs: targetScoreTimeMs,
|
||||||
restSecondsOverride: restSecondsOverride,
|
restSecondsOverride: restSecondsOverride,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -364,6 +391,7 @@ final class ProgramExercise {
|
|||||||
'availableTimeSnapshot': availableTimeSnapshot,
|
'availableTimeSnapshot': availableTimeSnapshot,
|
||||||
'availableRepsSnapshot': availableRepsSnapshot,
|
'availableRepsSnapshot': availableRepsSnapshot,
|
||||||
'availableScoreSnapshot': availableScoreSnapshot,
|
'availableScoreSnapshot': availableScoreSnapshot,
|
||||||
|
'scoreInputModeSnapshot': scoreInputModeSnapshot.name,
|
||||||
'scoreLabelSnapshot': scoreLabelSnapshot,
|
'scoreLabelSnapshot': scoreLabelSnapshot,
|
||||||
'scoreUnitSnapshot': scoreUnitSnapshot,
|
'scoreUnitSnapshot': scoreUnitSnapshot,
|
||||||
'setsCount': setsCount,
|
'setsCount': setsCount,
|
||||||
@ -373,6 +401,7 @@ final class ProgramExercise {
|
|||||||
'targetTimeSeconds': targetTimeSeconds,
|
'targetTimeSeconds': targetTimeSeconds,
|
||||||
'targetReps': targetReps,
|
'targetReps': targetReps,
|
||||||
'targetScore': targetScore,
|
'targetScore': targetScore,
|
||||||
|
'targetScoreTimeMs': targetScoreTimeMs,
|
||||||
'restSecondsOverride': restSecondsOverride,
|
'restSecondsOverride': restSecondsOverride,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -452,6 +481,7 @@ final class WorkoutTemplateExerciseOverride {
|
|||||||
this.targetTimeSecondsOverride,
|
this.targetTimeSecondsOverride,
|
||||||
this.targetRepsOverride,
|
this.targetRepsOverride,
|
||||||
this.targetScoreOverride,
|
this.targetScoreOverride,
|
||||||
|
this.targetScoreTimeMsOverride,
|
||||||
}) : snapshotProgramExerciseId = _nonBlank(
|
}) : snapshotProgramExerciseId = _nonBlank(
|
||||||
snapshotProgramExerciseId,
|
snapshotProgramExerciseId,
|
||||||
'Snapshot program exercise id',
|
'Snapshot program exercise id',
|
||||||
@ -466,6 +496,15 @@ final class WorkoutTemplateExerciseOverride {
|
|||||||
targetScoreOverride,
|
targetScoreOverride,
|
||||||
'Target score override',
|
'Target score override',
|
||||||
);
|
);
|
||||||
|
_requireNullablePositive(
|
||||||
|
targetScoreTimeMsOverride,
|
||||||
|
'Target score time ms override',
|
||||||
|
);
|
||||||
|
if (targetScoreOverride != null && targetScoreTimeMsOverride != null) {
|
||||||
|
throw const DomainException(
|
||||||
|
'Manual and stopwatch score overrides cannot both be set.',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final EntityMetadata metadata;
|
final EntityMetadata metadata;
|
||||||
@ -475,6 +514,7 @@ final class WorkoutTemplateExerciseOverride {
|
|||||||
final int? targetTimeSecondsOverride;
|
final int? targetTimeSecondsOverride;
|
||||||
final int? targetRepsOverride;
|
final int? targetRepsOverride;
|
||||||
final double? targetScoreOverride;
|
final double? targetScoreOverride;
|
||||||
|
final int? targetScoreTimeMsOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ActiveWorkoutSession {
|
final class ActiveWorkoutSession {
|
||||||
@ -587,15 +627,25 @@ final class ActiveSetResult {
|
|||||||
this.actualTimeMs,
|
this.actualTimeMs,
|
||||||
this.actualReps,
|
this.actualReps,
|
||||||
this.actualScore,
|
this.actualScore,
|
||||||
|
this.actualScoreTimeMs,
|
||||||
|
this.scoreInputModeSnapshot = ScoreInputMode.manual,
|
||||||
this.scoreLabelSnapshot,
|
this.scoreLabelSnapshot,
|
||||||
this.scoreUnitSnapshot,
|
this.scoreUnitSnapshot,
|
||||||
this.note,
|
this.note,
|
||||||
this.status = SetResultStatus.completed,
|
this.status = SetResultStatus.completed,
|
||||||
}) {
|
}) {
|
||||||
if (status == SetResultStatus.skipped &&
|
if (status == SetResultStatus.skipped &&
|
||||||
(actualTimeMs != null || actualReps != null || actualScore != null)) {
|
(actualTimeMs != null ||
|
||||||
|
actualReps != null ||
|
||||||
|
actualScore != null ||
|
||||||
|
actualScoreTimeMs != null)) {
|
||||||
throw const DomainException('Skipped set results cannot have values.');
|
throw const DomainException('Skipped set results cannot have values.');
|
||||||
}
|
}
|
||||||
|
_requireScoreResultShape(
|
||||||
|
scoreInputMode: scoreInputModeSnapshot,
|
||||||
|
actualScore: actualScore,
|
||||||
|
actualScoreTimeMs: actualScoreTimeMs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final EntityMetadata metadata;
|
final EntityMetadata metadata;
|
||||||
@ -610,12 +660,72 @@ final class ActiveSetResult {
|
|||||||
final int? actualTimeMs;
|
final int? actualTimeMs;
|
||||||
final int? actualReps;
|
final int? actualReps;
|
||||||
final double? actualScore;
|
final double? actualScore;
|
||||||
|
final int? actualScoreTimeMs;
|
||||||
|
final ScoreInputMode scoreInputModeSnapshot;
|
||||||
final String? scoreLabelSnapshot;
|
final String? scoreLabelSnapshot;
|
||||||
final String? scoreUnitSnapshot;
|
final String? scoreUnitSnapshot;
|
||||||
final String? note;
|
final String? note;
|
||||||
final SetResultStatus status;
|
final SetResultStatus status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class ActiveScoreStopwatchState {
|
||||||
|
ActiveScoreStopwatchState({
|
||||||
|
required this.metadata,
|
||||||
|
required this.activeWorkoutSessionId,
|
||||||
|
required this.programIndex,
|
||||||
|
required this.exerciseIndex,
|
||||||
|
required this.setIndex,
|
||||||
|
required this.status,
|
||||||
|
required this.startedAt,
|
||||||
|
required this.accumulatedMs,
|
||||||
|
this.stoppedAt,
|
||||||
|
}) {
|
||||||
|
_requireNonNegative(programIndex, 'Program index');
|
||||||
|
_requireNonNegative(exerciseIndex, 'Exercise index');
|
||||||
|
_requireNonNegative(setIndex, 'Set index');
|
||||||
|
_requireNonNegative(accumulatedMs, 'Accumulated milliseconds');
|
||||||
|
}
|
||||||
|
|
||||||
|
final EntityMetadata metadata;
|
||||||
|
final String activeWorkoutSessionId;
|
||||||
|
final int programIndex;
|
||||||
|
final int exerciseIndex;
|
||||||
|
final int setIndex;
|
||||||
|
final ActiveScoreStopwatchStatus status;
|
||||||
|
final DateTime startedAt;
|
||||||
|
final int accumulatedMs;
|
||||||
|
final DateTime? stoppedAt;
|
||||||
|
|
||||||
|
int elapsedMillisecondsAt(DateTime now) {
|
||||||
|
if (status == ActiveScoreStopwatchStatus.stopped) {
|
||||||
|
return accumulatedMs;
|
||||||
|
}
|
||||||
|
return accumulatedMs + now.difference(startedAt).inMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveScoreStopwatchState copyWith({
|
||||||
|
EntityMetadata? metadata,
|
||||||
|
ActiveScoreStopwatchStatus? status,
|
||||||
|
DateTime? startedAt,
|
||||||
|
int? accumulatedMs,
|
||||||
|
Object? stoppedAt = _unchanged,
|
||||||
|
}) {
|
||||||
|
return ActiveScoreStopwatchState(
|
||||||
|
metadata: metadata ?? this.metadata,
|
||||||
|
activeWorkoutSessionId: activeWorkoutSessionId,
|
||||||
|
programIndex: programIndex,
|
||||||
|
exerciseIndex: exerciseIndex,
|
||||||
|
setIndex: setIndex,
|
||||||
|
status: status ?? this.status,
|
||||||
|
startedAt: startedAt ?? this.startedAt,
|
||||||
|
accumulatedMs: accumulatedMs ?? this.accumulatedMs,
|
||||||
|
stoppedAt: stoppedAt == _unchanged
|
||||||
|
? this.stoppedAt
|
||||||
|
: stoppedAt as DateTime?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class ActiveRestState {
|
final class ActiveRestState {
|
||||||
const ActiveRestState({
|
const ActiveRestState({
|
||||||
required this.metadata,
|
required this.metadata,
|
||||||
@ -707,9 +817,12 @@ final class WorkoutHistorySetResult {
|
|||||||
this.targetTimeSecondsSnapshot,
|
this.targetTimeSecondsSnapshot,
|
||||||
this.targetRepsSnapshot,
|
this.targetRepsSnapshot,
|
||||||
this.targetScoreSnapshot,
|
this.targetScoreSnapshot,
|
||||||
|
this.targetScoreTimeMsSnapshot,
|
||||||
this.actualTimeMs,
|
this.actualTimeMs,
|
||||||
this.actualReps,
|
this.actualReps,
|
||||||
this.actualScore,
|
this.actualScore,
|
||||||
|
this.actualScoreTimeMs,
|
||||||
|
this.scoreInputModeSnapshot = ScoreInputMode.manual,
|
||||||
this.scoreLabelSnapshot,
|
this.scoreLabelSnapshot,
|
||||||
this.scoreUnitSnapshot,
|
this.scoreUnitSnapshot,
|
||||||
this.startedAt,
|
this.startedAt,
|
||||||
@ -717,11 +830,25 @@ final class WorkoutHistorySetResult {
|
|||||||
this.status = SetResultStatus.completed,
|
this.status = SetResultStatus.completed,
|
||||||
}) {
|
}) {
|
||||||
if (status == SetResultStatus.skipped &&
|
if (status == SetResultStatus.skipped &&
|
||||||
(actualTimeMs != null || actualReps != null || actualScore != null)) {
|
(actualTimeMs != null ||
|
||||||
|
actualReps != null ||
|
||||||
|
actualScore != null ||
|
||||||
|
actualScoreTimeMs != null)) {
|
||||||
throw const DomainException(
|
throw const DomainException(
|
||||||
'Skipped history results cannot have values.',
|
'Skipped history results cannot have values.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
_requireScoreTargetShape(
|
||||||
|
scoreEnabled: scoreEnabledSnapshot,
|
||||||
|
scoreInputMode: scoreInputModeSnapshot,
|
||||||
|
targetScore: targetScoreSnapshot,
|
||||||
|
targetScoreTimeMs: targetScoreTimeMsSnapshot,
|
||||||
|
);
|
||||||
|
_requireScoreResultShape(
|
||||||
|
scoreInputMode: scoreInputModeSnapshot,
|
||||||
|
actualScore: actualScore,
|
||||||
|
actualScoreTimeMs: actualScoreTimeMs,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final EntityMetadata metadata;
|
final EntityMetadata metadata;
|
||||||
@ -739,9 +866,12 @@ final class WorkoutHistorySetResult {
|
|||||||
final int? targetTimeSecondsSnapshot;
|
final int? targetTimeSecondsSnapshot;
|
||||||
final int? targetRepsSnapshot;
|
final int? targetRepsSnapshot;
|
||||||
final double? targetScoreSnapshot;
|
final double? targetScoreSnapshot;
|
||||||
|
final int? targetScoreTimeMsSnapshot;
|
||||||
final int? actualTimeMs;
|
final int? actualTimeMs;
|
||||||
final int? actualReps;
|
final int? actualReps;
|
||||||
final double? actualScore;
|
final double? actualScore;
|
||||||
|
final int? actualScoreTimeMs;
|
||||||
|
final ScoreInputMode scoreInputModeSnapshot;
|
||||||
final String? scoreLabelSnapshot;
|
final String? scoreLabelSnapshot;
|
||||||
final String? scoreUnitSnapshot;
|
final String? scoreUnitSnapshot;
|
||||||
final DateTime? startedAt;
|
final DateTime? startedAt;
|
||||||
@ -796,3 +926,50 @@ void _requireNullableNonNegativeDouble(double? value, String label) {
|
|||||||
throw DomainException('$label must not be negative.');
|
throw DomainException('$label must not be negative.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _requireScoreTargetShape({
|
||||||
|
required bool scoreEnabled,
|
||||||
|
required ScoreInputMode scoreInputMode,
|
||||||
|
required double? targetScore,
|
||||||
|
required int? targetScoreTimeMs,
|
||||||
|
}) {
|
||||||
|
if (targetScore != null && targetScoreTimeMs != null) {
|
||||||
|
throw const DomainException(
|
||||||
|
'Manual and stopwatch score targets cannot both be set.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (targetScore != null &&
|
||||||
|
(!scoreEnabled || scoreInputMode != ScoreInputMode.manual)) {
|
||||||
|
throw const DomainException(
|
||||||
|
'Manual score target requires manual score mode.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (targetScoreTimeMs != null &&
|
||||||
|
(!scoreEnabled || scoreInputMode != ScoreInputMode.stopwatch)) {
|
||||||
|
throw const DomainException(
|
||||||
|
'Stopwatch score target requires stopwatch score mode.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _requireScoreResultShape({
|
||||||
|
required ScoreInputMode scoreInputMode,
|
||||||
|
required double? actualScore,
|
||||||
|
required int? actualScoreTimeMs,
|
||||||
|
}) {
|
||||||
|
if (actualScore != null && actualScoreTimeMs != null) {
|
||||||
|
throw const DomainException(
|
||||||
|
'Manual and stopwatch score results cannot both be set.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (actualScore != null && scoreInputMode != ScoreInputMode.manual) {
|
||||||
|
throw const DomainException(
|
||||||
|
'Manual score result requires manual score mode.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (actualScoreTimeMs != null && scoreInputMode != ScoreInputMode.stopwatch) {
|
||||||
|
throw const DomainException(
|
||||||
|
'Stopwatch score result requires stopwatch score mode.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ part 'app_database.g.dart';
|
|||||||
@DriftDatabase(
|
@DriftDatabase(
|
||||||
tables: [
|
tables: [
|
||||||
ActiveRestStates,
|
ActiveRestStates,
|
||||||
|
ActiveScoreStopwatchStates,
|
||||||
ActiveSetResults,
|
ActiveSetResults,
|
||||||
ActiveWorkoutSessions,
|
ActiveWorkoutSessions,
|
||||||
ChangeLogEntries,
|
ChangeLogEntries,
|
||||||
@ -35,7 +36,7 @@ final class AppDatabase extends _$AppDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 2;
|
int get schemaVersion => 3;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration {
|
MigrationStrategy get migration {
|
||||||
@ -56,6 +57,10 @@ final class AppDatabase extends _$AppDatabase {
|
|||||||
"'skipped'))",
|
"'skipped'))",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (from < 3) {
|
||||||
|
await _migrateToSchema3(migrator);
|
||||||
|
}
|
||||||
|
await _createIndexes();
|
||||||
},
|
},
|
||||||
beforeOpen: (details) async {
|
beforeOpen: (details) async {
|
||||||
await customStatement('PRAGMA foreign_keys = ON');
|
await customStatement('PRAGMA foreign_keys = ON');
|
||||||
@ -95,6 +100,10 @@ final class AppDatabase extends _$AppDatabase {
|
|||||||
'CREATE INDEX IF NOT EXISTS idx_active_set_results_session_id '
|
'CREATE INDEX IF NOT EXISTS idx_active_set_results_session_id '
|
||||||
'ON active_set_results (active_workout_session_id)',
|
'ON active_set_results (active_workout_session_id)',
|
||||||
);
|
);
|
||||||
|
await customStatement(
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_active_score_stopwatch_states_session_id '
|
||||||
|
'ON active_score_stopwatch_states (active_workout_session_id)',
|
||||||
|
);
|
||||||
await customStatement(
|
await customStatement(
|
||||||
'CREATE INDEX IF NOT EXISTS idx_active_rest_states_session_id '
|
'CREATE INDEX IF NOT EXISTS idx_active_rest_states_session_id '
|
||||||
'ON active_rest_states (active_workout_session_id)',
|
'ON active_rest_states (active_workout_session_id)',
|
||||||
@ -131,6 +140,7 @@ final class AppDatabase extends _$AppDatabase {
|
|||||||
|
|
||||||
const _syncableTableNames = [
|
const _syncableTableNames = [
|
||||||
'active_rest_states',
|
'active_rest_states',
|
||||||
|
'active_score_stopwatch_states',
|
||||||
'active_set_results',
|
'active_set_results',
|
||||||
'active_workout_sessions',
|
'active_workout_sessions',
|
||||||
'exercises',
|
'exercises',
|
||||||
@ -143,3 +153,53 @@ const _syncableTableNames = [
|
|||||||
'workout_template_programs',
|
'workout_template_programs',
|
||||||
'workout_templates',
|
'workout_templates',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
extension on AppDatabase {
|
||||||
|
Future<void> _migrateToSchema3(Migrator migrator) async {
|
||||||
|
await customStatement(
|
||||||
|
'ALTER TABLE exercises ADD COLUMN score_input_mode TEXT NOT NULL '
|
||||||
|
"DEFAULT 'manual' CHECK (score_input_mode IN ('manual', 'stopwatch'))",
|
||||||
|
);
|
||||||
|
await customStatement(
|
||||||
|
'ALTER TABLE program_exercises ADD COLUMN score_input_mode_snapshot TEXT '
|
||||||
|
"NOT NULL DEFAULT 'manual' CHECK (score_input_mode_snapshot IN "
|
||||||
|
"('manual', 'stopwatch'))",
|
||||||
|
);
|
||||||
|
await customStatement(
|
||||||
|
'ALTER TABLE program_exercises ADD COLUMN target_score_time_ms INTEGER '
|
||||||
|
'CHECK (target_score_time_ms IS NULL OR target_score_time_ms > 0)',
|
||||||
|
);
|
||||||
|
await customStatement(
|
||||||
|
'ALTER TABLE workout_template_exercise_overrides ADD COLUMN '
|
||||||
|
'target_score_time_ms_override INTEGER CHECK '
|
||||||
|
'(target_score_time_ms_override IS NULL OR '
|
||||||
|
'target_score_time_ms_override > 0)',
|
||||||
|
);
|
||||||
|
await customStatement(
|
||||||
|
'ALTER TABLE active_set_results ADD COLUMN actual_score_time_ms INTEGER '
|
||||||
|
'CHECK (actual_score_time_ms IS NULL OR actual_score_time_ms >= 0)',
|
||||||
|
);
|
||||||
|
await customStatement(
|
||||||
|
'ALTER TABLE active_set_results ADD COLUMN score_input_mode_snapshot '
|
||||||
|
"TEXT NOT NULL DEFAULT 'manual' CHECK (score_input_mode_snapshot IN "
|
||||||
|
"('manual', 'stopwatch'))",
|
||||||
|
);
|
||||||
|
await customStatement(
|
||||||
|
'ALTER TABLE workout_history_set_results ADD COLUMN '
|
||||||
|
'score_input_mode_snapshot TEXT NOT NULL DEFAULT '
|
||||||
|
"'manual' CHECK (score_input_mode_snapshot IN ('manual', 'stopwatch'))",
|
||||||
|
);
|
||||||
|
await customStatement(
|
||||||
|
'ALTER TABLE workout_history_set_results ADD COLUMN '
|
||||||
|
'target_score_time_ms_snapshot INTEGER CHECK '
|
||||||
|
'(target_score_time_ms_snapshot IS NULL OR '
|
||||||
|
'target_score_time_ms_snapshot > 0)',
|
||||||
|
);
|
||||||
|
await customStatement(
|
||||||
|
'ALTER TABLE workout_history_set_results ADD COLUMN '
|
||||||
|
'actual_score_time_ms INTEGER CHECK (actual_score_time_ms IS NULL OR '
|
||||||
|
'actual_score_time_ms >= 0)',
|
||||||
|
);
|
||||||
|
await migrator.createTable(activeScoreStopwatchStates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -469,6 +469,77 @@ final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
return row == null ? null : _activeRestStateFromRow(row);
|
return row == null ? null : _activeRestStateFromRow(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveScoreStopwatchState(
|
||||||
|
domain.ActiveScoreStopwatchState state,
|
||||||
|
) async {
|
||||||
|
await _upsertWithChangeLog(
|
||||||
|
database: database,
|
||||||
|
tableName: 'active_score_stopwatch_states',
|
||||||
|
entityType: 'ActiveScoreStopwatchState',
|
||||||
|
metadata: state.metadata,
|
||||||
|
write: () => database
|
||||||
|
.into(database.activeScoreStopwatchStates)
|
||||||
|
.insertOnConflictUpdate(_activeScoreStopwatchStateCompanion(state)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteScoreStopwatchState({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
required DateTime deletedAt,
|
||||||
|
}) async {
|
||||||
|
final row =
|
||||||
|
await (database.select(database.activeScoreStopwatchStates)..where(
|
||||||
|
(table) =>
|
||||||
|
table.activeWorkoutSessionId.equals(sessionId) &
|
||||||
|
table.programIndex.equals(programIndex) &
|
||||||
|
table.exerciseIndex.equals(exerciseIndex) &
|
||||||
|
table.setIndex.equals(setIndex) &
|
||||||
|
table.deletedAt.isNull(),
|
||||||
|
))
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (row == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final revision = row.localRevision + 1;
|
||||||
|
await (database.delete(
|
||||||
|
database.activeScoreStopwatchStates,
|
||||||
|
)..where((table) => table.id.equals(row.id))).go();
|
||||||
|
await _writeChangeLog(
|
||||||
|
database: database,
|
||||||
|
entityType: 'ActiveScoreStopwatchState',
|
||||||
|
entityId: row.id,
|
||||||
|
operation: 'delete',
|
||||||
|
localRevision: revision,
|
||||||
|
originDeviceId: row.originDeviceId,
|
||||||
|
createdAt: deletedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<domain.ActiveScoreStopwatchState?> findScoreStopwatchState({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
}) async {
|
||||||
|
final row =
|
||||||
|
await (database.select(database.activeScoreStopwatchStates)..where(
|
||||||
|
(table) =>
|
||||||
|
table.activeWorkoutSessionId.equals(sessionId) &
|
||||||
|
table.programIndex.equals(programIndex) &
|
||||||
|
table.exerciseIndex.equals(exerciseIndex) &
|
||||||
|
table.setIndex.equals(setIndex) &
|
||||||
|
table.deletedAt.isNull(),
|
||||||
|
))
|
||||||
|
.getSingleOrNull();
|
||||||
|
return row == null ? null : _activeScoreStopwatchStateFromRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<domain.ActiveSetResult>> listSetResults(String sessionId) async {
|
Future<List<domain.ActiveSetResult>> listSetResults(String sessionId) async {
|
||||||
final rows =
|
final rows =
|
||||||
@ -492,6 +563,26 @@ final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
.get();
|
.get();
|
||||||
return rows.map(_activeRestStateFromRow).toList();
|
return rows.map(_activeRestStateFromRow).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<domain.ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
|
final rows =
|
||||||
|
await (database.select(database.activeScoreStopwatchStates)
|
||||||
|
..where(
|
||||||
|
(table) =>
|
||||||
|
table.activeWorkoutSessionId.equals(sessionId) &
|
||||||
|
table.deletedAt.isNull(),
|
||||||
|
)
|
||||||
|
..orderBy([
|
||||||
|
(table) => OrderingTerm.asc(table.programIndex),
|
||||||
|
(table) => OrderingTerm.asc(table.exerciseIndex),
|
||||||
|
(table) => OrderingTerm.asc(table.setIndex),
|
||||||
|
]))
|
||||||
|
.get();
|
||||||
|
return rows.map(_activeScoreStopwatchStateFromRow).toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class DriftWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
final class DriftWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||||
@ -872,6 +963,7 @@ db.ExercisesCompanion _exerciseCompanion(domain.Exercise exercise) {
|
|||||||
hasTimeMeasure: Value(exercise.hasTimeMeasure),
|
hasTimeMeasure: Value(exercise.hasTimeMeasure),
|
||||||
hasRepsMeasure: Value(exercise.hasRepsMeasure),
|
hasRepsMeasure: Value(exercise.hasRepsMeasure),
|
||||||
hasScoreMeasure: Value(exercise.hasScoreMeasure),
|
hasScoreMeasure: Value(exercise.hasScoreMeasure),
|
||||||
|
scoreInputMode: Value(_scoreInputModeToDb(exercise.scoreInputMode)),
|
||||||
scoreLabel: Value(exercise.scoreLabel),
|
scoreLabel: Value(exercise.scoreLabel),
|
||||||
scoreUnit: Value(exercise.scoreUnit),
|
scoreUnit: Value(exercise.scoreUnit),
|
||||||
archivedAt: Value(_utcOrNull(exercise.archivedAt)),
|
archivedAt: Value(_utcOrNull(exercise.archivedAt)),
|
||||||
@ -888,6 +980,7 @@ domain.Exercise _exerciseFromRow(db.Exercise row) {
|
|||||||
hasTimeMeasure: row.hasTimeMeasure,
|
hasTimeMeasure: row.hasTimeMeasure,
|
||||||
hasRepsMeasure: row.hasRepsMeasure,
|
hasRepsMeasure: row.hasRepsMeasure,
|
||||||
hasScoreMeasure: row.hasScoreMeasure,
|
hasScoreMeasure: row.hasScoreMeasure,
|
||||||
|
scoreInputMode: _scoreInputModeFromDb(row.scoreInputMode),
|
||||||
scoreLabel: row.scoreLabel,
|
scoreLabel: row.scoreLabel,
|
||||||
scoreUnit: row.scoreUnit,
|
scoreUnit: row.scoreUnit,
|
||||||
archivedAt: _utcOrNull(row.archivedAt),
|
archivedAt: _utcOrNull(row.archivedAt),
|
||||||
@ -995,6 +1088,9 @@ db.ProgramExercisesCompanion _programExerciseCompanion(
|
|||||||
availableTimeSnapshot: Value(exercise.availableTimeSnapshot),
|
availableTimeSnapshot: Value(exercise.availableTimeSnapshot),
|
||||||
availableRepsSnapshot: Value(exercise.availableRepsSnapshot),
|
availableRepsSnapshot: Value(exercise.availableRepsSnapshot),
|
||||||
availableScoreSnapshot: Value(exercise.availableScoreSnapshot),
|
availableScoreSnapshot: Value(exercise.availableScoreSnapshot),
|
||||||
|
scoreInputModeSnapshot: Value(
|
||||||
|
_scoreInputModeToDb(exercise.scoreInputModeSnapshot),
|
||||||
|
),
|
||||||
scoreLabelSnapshot: Value(exercise.scoreLabelSnapshot),
|
scoreLabelSnapshot: Value(exercise.scoreLabelSnapshot),
|
||||||
scoreUnitSnapshot: Value(exercise.scoreUnitSnapshot),
|
scoreUnitSnapshot: Value(exercise.scoreUnitSnapshot),
|
||||||
setsCount: Value(exercise.setsCount),
|
setsCount: Value(exercise.setsCount),
|
||||||
@ -1004,6 +1100,7 @@ db.ProgramExercisesCompanion _programExerciseCompanion(
|
|||||||
targetTimeSeconds: Value(exercise.targetTimeSeconds),
|
targetTimeSeconds: Value(exercise.targetTimeSeconds),
|
||||||
targetReps: Value(exercise.targetReps),
|
targetReps: Value(exercise.targetReps),
|
||||||
targetScore: Value(exercise.targetScore),
|
targetScore: Value(exercise.targetScore),
|
||||||
|
targetScoreTimeMs: Value(exercise.targetScoreTimeMs),
|
||||||
restSecondsOverride: Value(exercise.restSecondsOverride),
|
restSecondsOverride: Value(exercise.restSecondsOverride),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1022,6 +1119,7 @@ domain.ProgramExercise _programExerciseFromRow(db.ProgramExercise row) {
|
|||||||
availableTimeSnapshot: row.availableTimeSnapshot,
|
availableTimeSnapshot: row.availableTimeSnapshot,
|
||||||
availableRepsSnapshot: row.availableRepsSnapshot,
|
availableRepsSnapshot: row.availableRepsSnapshot,
|
||||||
availableScoreSnapshot: row.availableScoreSnapshot,
|
availableScoreSnapshot: row.availableScoreSnapshot,
|
||||||
|
scoreInputModeSnapshot: _scoreInputModeFromDb(row.scoreInputModeSnapshot),
|
||||||
scoreLabelSnapshot: row.scoreLabelSnapshot,
|
scoreLabelSnapshot: row.scoreLabelSnapshot,
|
||||||
scoreUnitSnapshot: row.scoreUnitSnapshot,
|
scoreUnitSnapshot: row.scoreUnitSnapshot,
|
||||||
setsCount: row.setsCount,
|
setsCount: row.setsCount,
|
||||||
@ -1031,6 +1129,7 @@ domain.ProgramExercise _programExerciseFromRow(db.ProgramExercise row) {
|
|||||||
targetTimeSeconds: row.targetTimeSeconds,
|
targetTimeSeconds: row.targetTimeSeconds,
|
||||||
targetReps: row.targetReps,
|
targetReps: row.targetReps,
|
||||||
targetScore: row.targetScore,
|
targetScore: row.targetScore,
|
||||||
|
targetScoreTimeMs: row.targetScoreTimeMs,
|
||||||
restSecondsOverride: row.restSecondsOverride,
|
restSecondsOverride: row.restSecondsOverride,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1132,6 +1231,7 @@ _workoutTemplateExerciseOverrideCompanion(
|
|||||||
targetTimeSecondsOverride: Value(override.targetTimeSecondsOverride),
|
targetTimeSecondsOverride: Value(override.targetTimeSecondsOverride),
|
||||||
targetRepsOverride: Value(override.targetRepsOverride),
|
targetRepsOverride: Value(override.targetRepsOverride),
|
||||||
targetScoreOverride: Value(override.targetScoreOverride),
|
targetScoreOverride: Value(override.targetScoreOverride),
|
||||||
|
targetScoreTimeMsOverride: Value(override.targetScoreTimeMsOverride),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1146,6 +1246,7 @@ domain.WorkoutTemplateExerciseOverride _workoutTemplateExerciseOverrideFromRow(
|
|||||||
targetTimeSecondsOverride: row.targetTimeSecondsOverride,
|
targetTimeSecondsOverride: row.targetTimeSecondsOverride,
|
||||||
targetRepsOverride: row.targetRepsOverride,
|
targetRepsOverride: row.targetRepsOverride,
|
||||||
targetScoreOverride: row.targetScoreOverride,
|
targetScoreOverride: row.targetScoreOverride,
|
||||||
|
targetScoreTimeMsOverride: row.targetScoreTimeMsOverride,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1225,6 +1326,10 @@ db.ActiveSetResultsCompanion _activeSetResultCompanion(
|
|||||||
actualTimeMs: Value(result.actualTimeMs),
|
actualTimeMs: Value(result.actualTimeMs),
|
||||||
actualReps: Value(result.actualReps),
|
actualReps: Value(result.actualReps),
|
||||||
actualScore: Value(result.actualScore),
|
actualScore: Value(result.actualScore),
|
||||||
|
actualScoreTimeMs: Value(result.actualScoreTimeMs),
|
||||||
|
scoreInputModeSnapshot: Value(
|
||||||
|
_scoreInputModeToDb(result.scoreInputModeSnapshot),
|
||||||
|
),
|
||||||
scoreLabelSnapshot: Value(result.scoreLabelSnapshot),
|
scoreLabelSnapshot: Value(result.scoreLabelSnapshot),
|
||||||
scoreUnitSnapshot: Value(result.scoreUnitSnapshot),
|
scoreUnitSnapshot: Value(result.scoreUnitSnapshot),
|
||||||
note: Value(result.note),
|
note: Value(result.note),
|
||||||
@ -1246,6 +1351,8 @@ domain.ActiveSetResult _activeSetResultFromRow(db.ActiveSetResult row) {
|
|||||||
actualTimeMs: row.actualTimeMs,
|
actualTimeMs: row.actualTimeMs,
|
||||||
actualReps: row.actualReps,
|
actualReps: row.actualReps,
|
||||||
actualScore: row.actualScore,
|
actualScore: row.actualScore,
|
||||||
|
actualScoreTimeMs: row.actualScoreTimeMs,
|
||||||
|
scoreInputModeSnapshot: _scoreInputModeFromDb(row.scoreInputModeSnapshot),
|
||||||
scoreLabelSnapshot: row.scoreLabelSnapshot,
|
scoreLabelSnapshot: row.scoreLabelSnapshot,
|
||||||
scoreUnitSnapshot: row.scoreUnitSnapshot,
|
scoreUnitSnapshot: row.scoreUnitSnapshot,
|
||||||
note: row.note,
|
note: row.note,
|
||||||
@ -1296,6 +1403,49 @@ domain.ActiveRestState _activeRestStateFromRow(db.ActiveRestState row) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db.ActiveScoreStopwatchStatesCompanion _activeScoreStopwatchStateCompanion(
|
||||||
|
domain.ActiveScoreStopwatchState state,
|
||||||
|
) {
|
||||||
|
final values = _metadataValues(state.metadata);
|
||||||
|
return db.ActiveScoreStopwatchStatesCompanion(
|
||||||
|
id: values[0] as Value<String>,
|
||||||
|
createdAt: values[1] as Value<DateTime>,
|
||||||
|
updatedAt: values[2] as Value<DateTime>,
|
||||||
|
deletedAt: values[3] as Value<DateTime?>,
|
||||||
|
schemaVersion: values[4] as Value<int>,
|
||||||
|
syncState: values[5] as Value<String>,
|
||||||
|
localRevision: values[6] as Value<int>,
|
||||||
|
originDeviceId: values[7] as Value<String>,
|
||||||
|
futureOwnerProfileId: values[8] as Value<String?>,
|
||||||
|
lastSyncedAt: values[9] as Value<DateTime?>,
|
||||||
|
remoteRevision: values[10] as Value<String?>,
|
||||||
|
activeWorkoutSessionId: Value(state.activeWorkoutSessionId),
|
||||||
|
programIndex: Value(state.programIndex),
|
||||||
|
exerciseIndex: Value(state.exerciseIndex),
|
||||||
|
setIndex: Value(state.setIndex),
|
||||||
|
status: Value(_scoreStopwatchStatusToDb(state.status)),
|
||||||
|
startedAt: Value(state.startedAt.toUtc()),
|
||||||
|
accumulatedMs: Value(state.accumulatedMs),
|
||||||
|
stoppedAt: Value(_utcOrNull(state.stoppedAt)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.ActiveScoreStopwatchState _activeScoreStopwatchStateFromRow(
|
||||||
|
db.ActiveScoreStopwatchState row,
|
||||||
|
) {
|
||||||
|
return domain.ActiveScoreStopwatchState(
|
||||||
|
metadata: _metadataFromRow(row),
|
||||||
|
activeWorkoutSessionId: row.activeWorkoutSessionId,
|
||||||
|
programIndex: row.programIndex,
|
||||||
|
exerciseIndex: row.exerciseIndex,
|
||||||
|
setIndex: row.setIndex,
|
||||||
|
status: _scoreStopwatchStatusFromDb(row.status),
|
||||||
|
startedAt: _utc(row.startedAt),
|
||||||
|
accumulatedMs: row.accumulatedMs,
|
||||||
|
stoppedAt: _utcOrNull(row.stoppedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
db.WorkoutHistoriesCompanion _workoutHistoryCompanion(
|
db.WorkoutHistoriesCompanion _workoutHistoryCompanion(
|
||||||
domain.WorkoutHistory history,
|
domain.WorkoutHistory history,
|
||||||
) {
|
) {
|
||||||
@ -1350,12 +1500,17 @@ db.WorkoutHistorySetResultsCompanion _workoutHistorySetResultCompanion(
|
|||||||
timeEnabledSnapshot: Value(result.timeEnabledSnapshot),
|
timeEnabledSnapshot: Value(result.timeEnabledSnapshot),
|
||||||
repsEnabledSnapshot: Value(result.repsEnabledSnapshot),
|
repsEnabledSnapshot: Value(result.repsEnabledSnapshot),
|
||||||
scoreEnabledSnapshot: Value(result.scoreEnabledSnapshot),
|
scoreEnabledSnapshot: Value(result.scoreEnabledSnapshot),
|
||||||
|
scoreInputModeSnapshot: Value(
|
||||||
|
_scoreInputModeToDb(result.scoreInputModeSnapshot),
|
||||||
|
),
|
||||||
targetTimeSecondsSnapshot: Value(result.targetTimeSecondsSnapshot),
|
targetTimeSecondsSnapshot: Value(result.targetTimeSecondsSnapshot),
|
||||||
targetRepsSnapshot: Value(result.targetRepsSnapshot),
|
targetRepsSnapshot: Value(result.targetRepsSnapshot),
|
||||||
targetScoreSnapshot: Value(result.targetScoreSnapshot),
|
targetScoreSnapshot: Value(result.targetScoreSnapshot),
|
||||||
|
targetScoreTimeMsSnapshot: Value(result.targetScoreTimeMsSnapshot),
|
||||||
actualTimeMs: Value(result.actualTimeMs),
|
actualTimeMs: Value(result.actualTimeMs),
|
||||||
actualReps: Value(result.actualReps),
|
actualReps: Value(result.actualReps),
|
||||||
actualScore: Value(result.actualScore),
|
actualScore: Value(result.actualScore),
|
||||||
|
actualScoreTimeMs: Value(result.actualScoreTimeMs),
|
||||||
scoreLabelSnapshot: Value(result.scoreLabelSnapshot),
|
scoreLabelSnapshot: Value(result.scoreLabelSnapshot),
|
||||||
scoreUnitSnapshot: Value(result.scoreUnitSnapshot),
|
scoreUnitSnapshot: Value(result.scoreUnitSnapshot),
|
||||||
startedAt: Value(_utcOrNull(result.startedAt)),
|
startedAt: Value(_utcOrNull(result.startedAt)),
|
||||||
@ -1398,12 +1553,15 @@ domain.WorkoutHistorySetResult _workoutHistorySetResultFromRow(
|
|||||||
timeEnabledSnapshot: row.timeEnabledSnapshot,
|
timeEnabledSnapshot: row.timeEnabledSnapshot,
|
||||||
repsEnabledSnapshot: row.repsEnabledSnapshot,
|
repsEnabledSnapshot: row.repsEnabledSnapshot,
|
||||||
scoreEnabledSnapshot: row.scoreEnabledSnapshot,
|
scoreEnabledSnapshot: row.scoreEnabledSnapshot,
|
||||||
|
scoreInputModeSnapshot: _scoreInputModeFromDb(row.scoreInputModeSnapshot),
|
||||||
targetTimeSecondsSnapshot: row.targetTimeSecondsSnapshot,
|
targetTimeSecondsSnapshot: row.targetTimeSecondsSnapshot,
|
||||||
targetRepsSnapshot: row.targetRepsSnapshot,
|
targetRepsSnapshot: row.targetRepsSnapshot,
|
||||||
targetScoreSnapshot: row.targetScoreSnapshot,
|
targetScoreSnapshot: row.targetScoreSnapshot,
|
||||||
|
targetScoreTimeMsSnapshot: row.targetScoreTimeMsSnapshot,
|
||||||
actualTimeMs: row.actualTimeMs,
|
actualTimeMs: row.actualTimeMs,
|
||||||
actualReps: row.actualReps,
|
actualReps: row.actualReps,
|
||||||
actualScore: row.actualScore,
|
actualScore: row.actualScore,
|
||||||
|
actualScoreTimeMs: row.actualScoreTimeMs,
|
||||||
scoreLabelSnapshot: row.scoreLabelSnapshot,
|
scoreLabelSnapshot: row.scoreLabelSnapshot,
|
||||||
scoreUnitSnapshot: row.scoreUnitSnapshot,
|
scoreUnitSnapshot: row.scoreUnitSnapshot,
|
||||||
startedAt: _utcOrNull(row.startedAt),
|
startedAt: _utcOrNull(row.startedAt),
|
||||||
@ -1446,6 +1604,17 @@ domain.MediaKind _mediaKindFromDb(String value) => switch (value) {
|
|||||||
_ => throw domain.DomainException('Unknown media kind: $value'),
|
_ => throw domain.DomainException('Unknown media kind: $value'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
String _scoreInputModeToDb(domain.ScoreInputMode mode) => switch (mode) {
|
||||||
|
domain.ScoreInputMode.manual => 'manual',
|
||||||
|
domain.ScoreInputMode.stopwatch => 'stopwatch',
|
||||||
|
};
|
||||||
|
|
||||||
|
domain.ScoreInputMode _scoreInputModeFromDb(String value) => switch (value) {
|
||||||
|
'manual' => domain.ScoreInputMode.manual,
|
||||||
|
'stopwatch' => domain.ScoreInputMode.stopwatch,
|
||||||
|
_ => throw domain.DomainException('Unknown score input mode: $value'),
|
||||||
|
};
|
||||||
|
|
||||||
String _setResultStatusToDb(domain.SetResultStatus status) => switch (status) {
|
String _setResultStatusToDb(domain.SetResultStatus status) => switch (status) {
|
||||||
domain.SetResultStatus.completed => 'completed',
|
domain.SetResultStatus.completed => 'completed',
|
||||||
domain.SetResultStatus.skipped => 'skipped',
|
domain.SetResultStatus.skipped => 'skipped',
|
||||||
@ -1457,6 +1626,21 @@ domain.SetResultStatus _setResultStatusFromDb(String value) => switch (value) {
|
|||||||
_ => throw domain.DomainException('Unknown set result status: $value'),
|
_ => throw domain.DomainException('Unknown set result status: $value'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
String _scoreStopwatchStatusToDb(domain.ActiveScoreStopwatchStatus status) =>
|
||||||
|
switch (status) {
|
||||||
|
domain.ActiveScoreStopwatchStatus.running => 'running',
|
||||||
|
domain.ActiveScoreStopwatchStatus.stopped => 'stopped',
|
||||||
|
};
|
||||||
|
|
||||||
|
domain.ActiveScoreStopwatchStatus _scoreStopwatchStatusFromDb(String value) =>
|
||||||
|
switch (value) {
|
||||||
|
'running' => domain.ActiveScoreStopwatchStatus.running,
|
||||||
|
'stopped' => domain.ActiveScoreStopwatchStatus.stopped,
|
||||||
|
_ => throw domain.DomainException(
|
||||||
|
'Unknown active score stopwatch status: $value',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
domain.ActiveWorkoutStatus _activeStatusFromDb(String value) => switch (value) {
|
domain.ActiveWorkoutStatus _activeStatusFromDb(String value) => switch (value) {
|
||||||
'running' => domain.ActiveWorkoutStatus.running,
|
'running' => domain.ActiveWorkoutStatus.running,
|
||||||
'paused' => domain.ActiveWorkoutStatus.paused,
|
'paused' => domain.ActiveWorkoutStatus.paused,
|
||||||
|
|||||||
@ -65,6 +65,8 @@ class Exercises extends SyncableTable {
|
|||||||
BoolColumn get hasTimeMeasure => boolean()();
|
BoolColumn get hasTimeMeasure => boolean()();
|
||||||
BoolColumn get hasRepsMeasure => boolean()();
|
BoolColumn get hasRepsMeasure => boolean()();
|
||||||
BoolColumn get hasScoreMeasure => boolean()();
|
BoolColumn get hasScoreMeasure => boolean()();
|
||||||
|
TextColumn get scoreInputMode =>
|
||||||
|
text().withDefault(const Constant('manual'))();
|
||||||
TextColumn get scoreLabel => text().nullable()();
|
TextColumn get scoreLabel => text().nullable()();
|
||||||
TextColumn get scoreUnit => text().nullable()();
|
TextColumn get scoreUnit => text().nullable()();
|
||||||
DateTimeColumn get archivedAt => dateTime().nullable()();
|
DateTimeColumn get archivedAt => dateTime().nullable()();
|
||||||
@ -72,9 +74,11 @@ class Exercises extends SyncableTable {
|
|||||||
@override
|
@override
|
||||||
List<String> get customConstraints => [
|
List<String> get customConstraints => [
|
||||||
'CHECK (has_time_measure OR has_reps_measure OR has_score_measure)',
|
'CHECK (has_time_measure OR has_reps_measure OR has_score_measure)',
|
||||||
'CHECK (NOT has_score_measure OR (score_label IS NOT NULL '
|
"CHECK (score_input_mode IN ('manual', 'stopwatch'))",
|
||||||
'AND length(trim(score_label)) > 0 AND score_unit IS NOT NULL '
|
"CHECK (has_score_measure OR score_input_mode = 'manual')",
|
||||||
'AND length(trim(score_unit)) > 0))',
|
"CHECK (score_input_mode != 'manual' OR NOT has_score_measure OR "
|
||||||
|
'(score_label IS NOT NULL AND length(trim(score_label)) > 0 '
|
||||||
|
'AND score_unit IS NOT NULL AND length(trim(score_unit)) > 0))',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +115,8 @@ class ProgramExercises extends SyncableTable {
|
|||||||
BoolColumn get availableTimeSnapshot => boolean()();
|
BoolColumn get availableTimeSnapshot => boolean()();
|
||||||
BoolColumn get availableRepsSnapshot => boolean()();
|
BoolColumn get availableRepsSnapshot => boolean()();
|
||||||
BoolColumn get availableScoreSnapshot => boolean()();
|
BoolColumn get availableScoreSnapshot => boolean()();
|
||||||
|
TextColumn get scoreInputModeSnapshot =>
|
||||||
|
text().withDefault(const Constant('manual'))();
|
||||||
TextColumn get scoreLabelSnapshot => text().nullable()();
|
TextColumn get scoreLabelSnapshot => text().nullable()();
|
||||||
TextColumn get scoreUnitSnapshot => text().nullable()();
|
TextColumn get scoreUnitSnapshot => text().nullable()();
|
||||||
IntColumn get setsCount => integer()();
|
IntColumn get setsCount => integer()();
|
||||||
@ -120,6 +126,7 @@ class ProgramExercises extends SyncableTable {
|
|||||||
IntColumn get targetTimeSeconds => integer().nullable()();
|
IntColumn get targetTimeSeconds => integer().nullable()();
|
||||||
IntColumn get targetReps => integer().nullable()();
|
IntColumn get targetReps => integer().nullable()();
|
||||||
RealColumn get targetScore => real().nullable()();
|
RealColumn get targetScore => real().nullable()();
|
||||||
|
IntColumn get targetScoreTimeMs => integer().nullable()();
|
||||||
IntColumn get restSecondsOverride => integer().nullable()();
|
IntColumn get restSecondsOverride => integer().nullable()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -134,11 +141,18 @@ class ProgramExercises extends SyncableTable {
|
|||||||
'CHECK (target_time_seconds IS NULL OR target_time_seconds > 0)',
|
'CHECK (target_time_seconds IS NULL OR target_time_seconds > 0)',
|
||||||
'CHECK (target_reps IS NULL OR target_reps > 0)',
|
'CHECK (target_reps IS NULL OR target_reps > 0)',
|
||||||
'CHECK (target_score IS NULL OR target_score >= 0)',
|
'CHECK (target_score IS NULL OR target_score >= 0)',
|
||||||
|
'CHECK (target_score_time_ms IS NULL OR target_score_time_ms > 0)',
|
||||||
'CHECK (rest_seconds_override IS NULL OR rest_seconds_override >= 0)',
|
'CHECK (rest_seconds_override IS NULL OR rest_seconds_override >= 0)',
|
||||||
'CHECK (target_time_seconds IS NULL OR time_enabled)',
|
'CHECK (target_time_seconds IS NULL OR time_enabled)',
|
||||||
'CHECK (target_reps IS NULL OR reps_enabled)',
|
'CHECK (target_reps IS NULL OR reps_enabled)',
|
||||||
'CHECK (target_score IS NULL OR score_enabled)',
|
"CHECK (score_input_mode_snapshot IN ('manual', 'stopwatch'))",
|
||||||
'CHECK (NOT available_score_snapshot OR (score_label_snapshot '
|
'CHECK (target_score IS NULL OR '
|
||||||
|
"(score_enabled AND score_input_mode_snapshot = 'manual'))",
|
||||||
|
'CHECK (target_score_time_ms IS NULL OR '
|
||||||
|
"(score_enabled AND score_input_mode_snapshot = 'stopwatch'))",
|
||||||
|
'CHECK (target_score IS NULL OR target_score_time_ms IS NULL)',
|
||||||
|
"CHECK (score_input_mode_snapshot != 'manual' OR "
|
||||||
|
'NOT available_score_snapshot OR (score_label_snapshot '
|
||||||
'IS NOT NULL AND length(trim(score_label_snapshot)) > 0 '
|
'IS NOT NULL AND length(trim(score_label_snapshot)) > 0 '
|
||||||
'AND score_unit_snapshot IS NOT NULL '
|
'AND score_unit_snapshot IS NOT NULL '
|
||||||
'AND length(trim(score_unit_snapshot)) > 0))',
|
'AND length(trim(score_unit_snapshot)) > 0))',
|
||||||
@ -185,6 +199,7 @@ class WorkoutTemplateExerciseOverrides extends SyncableTable {
|
|||||||
IntColumn get targetTimeSecondsOverride => integer().nullable()();
|
IntColumn get targetTimeSecondsOverride => integer().nullable()();
|
||||||
IntColumn get targetRepsOverride => integer().nullable()();
|
IntColumn get targetRepsOverride => integer().nullable()();
|
||||||
RealColumn get targetScoreOverride => real().nullable()();
|
RealColumn get targetScoreOverride => real().nullable()();
|
||||||
|
IntColumn get targetScoreTimeMsOverride => integer().nullable()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> get customConstraints => [
|
List<String> get customConstraints => [
|
||||||
@ -194,6 +209,10 @@ class WorkoutTemplateExerciseOverrides extends SyncableTable {
|
|||||||
'target_time_seconds_override > 0)',
|
'target_time_seconds_override > 0)',
|
||||||
'CHECK (target_reps_override IS NULL OR target_reps_override > 0)',
|
'CHECK (target_reps_override IS NULL OR target_reps_override > 0)',
|
||||||
'CHECK (target_score_override IS NULL OR target_score_override >= 0)',
|
'CHECK (target_score_override IS NULL OR target_score_override >= 0)',
|
||||||
|
'CHECK (target_score_time_ms_override IS NULL OR '
|
||||||
|
'target_score_time_ms_override > 0)',
|
||||||
|
'CHECK (target_score_override IS NULL OR '
|
||||||
|
'target_score_time_ms_override IS NULL)',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,6 +260,9 @@ class ActiveSetResults extends SyncableTable {
|
|||||||
IntColumn get actualTimeMs => integer().nullable()();
|
IntColumn get actualTimeMs => integer().nullable()();
|
||||||
IntColumn get actualReps => integer().nullable()();
|
IntColumn get actualReps => integer().nullable()();
|
||||||
RealColumn get actualScore => real().nullable()();
|
RealColumn get actualScore => real().nullable()();
|
||||||
|
IntColumn get actualScoreTimeMs => integer().nullable()();
|
||||||
|
TextColumn get scoreInputModeSnapshot =>
|
||||||
|
text().withDefault(const Constant('manual'))();
|
||||||
TextColumn get scoreLabelSnapshot => text().nullable()();
|
TextColumn get scoreLabelSnapshot => text().nullable()();
|
||||||
TextColumn get scoreUnitSnapshot => text().nullable()();
|
TextColumn get scoreUnitSnapshot => text().nullable()();
|
||||||
TextColumn get note => text().nullable()();
|
TextColumn get note => text().nullable()();
|
||||||
@ -256,9 +278,17 @@ class ActiveSetResults extends SyncableTable {
|
|||||||
'CHECK (actual_time_ms IS NULL OR actual_time_ms >= 0)',
|
'CHECK (actual_time_ms IS NULL OR actual_time_ms >= 0)',
|
||||||
'CHECK (actual_reps IS NULL OR actual_reps >= 0)',
|
'CHECK (actual_reps IS NULL OR actual_reps >= 0)',
|
||||||
'CHECK (actual_score IS NULL OR actual_score >= 0)',
|
'CHECK (actual_score IS NULL OR actual_score >= 0)',
|
||||||
|
'CHECK (actual_score_time_ms IS NULL OR actual_score_time_ms >= 0)',
|
||||||
"CHECK (status IN ('completed', 'skipped'))",
|
"CHECK (status IN ('completed', 'skipped'))",
|
||||||
'CHECK (status != \'skipped\' OR (actual_time_ms IS NULL '
|
'CHECK (status != \'skipped\' OR (actual_time_ms IS NULL '
|
||||||
'AND actual_reps IS NULL AND actual_score IS NULL))',
|
'AND actual_reps IS NULL AND actual_score IS NULL '
|
||||||
|
'AND actual_score_time_ms IS NULL))',
|
||||||
|
"CHECK (score_input_mode_snapshot IN ('manual', 'stopwatch'))",
|
||||||
|
'CHECK (actual_score IS NULL OR '
|
||||||
|
"score_input_mode_snapshot = 'manual')",
|
||||||
|
'CHECK (actual_score_time_ms IS NULL OR '
|
||||||
|
"score_input_mode_snapshot = 'stopwatch')",
|
||||||
|
'CHECK (actual_score IS NULL OR actual_score_time_ms IS NULL)',
|
||||||
'CHECK (actual_score IS NULL OR (score_label_snapshot IS NOT NULL '
|
'CHECK (actual_score IS NULL OR (score_label_snapshot IS NOT NULL '
|
||||||
'AND length(trim(score_label_snapshot)) > 0 '
|
'AND length(trim(score_label_snapshot)) > 0 '
|
||||||
'AND score_unit_snapshot IS NOT NULL '
|
'AND score_unit_snapshot IS NOT NULL '
|
||||||
@ -266,6 +296,32 @@ class ActiveSetResults extends SyncableTable {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ActiveScoreStopwatchStates extends SyncableTable {
|
||||||
|
@override
|
||||||
|
String get tableName => 'active_score_stopwatch_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()();
|
||||||
|
IntColumn get accumulatedMs => integer()();
|
||||||
|
DateTimeColumn get stoppedAt => 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', 'stopped'))",
|
||||||
|
'CHECK (accumulated_ms >= 0)',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
class ActiveRestStates extends SyncableTable {
|
class ActiveRestStates extends SyncableTable {
|
||||||
@override
|
@override
|
||||||
String get tableName => 'active_rest_states';
|
String get tableName => 'active_rest_states';
|
||||||
@ -326,12 +382,16 @@ class WorkoutHistorySetResults extends SyncableTable {
|
|||||||
BoolColumn get timeEnabledSnapshot => boolean()();
|
BoolColumn get timeEnabledSnapshot => boolean()();
|
||||||
BoolColumn get repsEnabledSnapshot => boolean()();
|
BoolColumn get repsEnabledSnapshot => boolean()();
|
||||||
BoolColumn get scoreEnabledSnapshot => boolean()();
|
BoolColumn get scoreEnabledSnapshot => boolean()();
|
||||||
|
TextColumn get scoreInputModeSnapshot =>
|
||||||
|
text().withDefault(const Constant('manual'))();
|
||||||
IntColumn get targetTimeSecondsSnapshot => integer().nullable()();
|
IntColumn get targetTimeSecondsSnapshot => integer().nullable()();
|
||||||
IntColumn get targetRepsSnapshot => integer().nullable()();
|
IntColumn get targetRepsSnapshot => integer().nullable()();
|
||||||
RealColumn get targetScoreSnapshot => real().nullable()();
|
RealColumn get targetScoreSnapshot => real().nullable()();
|
||||||
|
IntColumn get targetScoreTimeMsSnapshot => integer().nullable()();
|
||||||
IntColumn get actualTimeMs => integer().nullable()();
|
IntColumn get actualTimeMs => integer().nullable()();
|
||||||
IntColumn get actualReps => integer().nullable()();
|
IntColumn get actualReps => integer().nullable()();
|
||||||
RealColumn get actualScore => real().nullable()();
|
RealColumn get actualScore => real().nullable()();
|
||||||
|
IntColumn get actualScoreTimeMs => integer().nullable()();
|
||||||
TextColumn get scoreLabelSnapshot => text().nullable()();
|
TextColumn get scoreLabelSnapshot => text().nullable()();
|
||||||
TextColumn get scoreUnitSnapshot => text().nullable()();
|
TextColumn get scoreUnitSnapshot => text().nullable()();
|
||||||
DateTimeColumn get startedAt => dateTime().nullable()();
|
DateTimeColumn get startedAt => dateTime().nullable()();
|
||||||
@ -351,12 +411,28 @@ class WorkoutHistorySetResults extends SyncableTable {
|
|||||||
'target_time_seconds_snapshot > 0)',
|
'target_time_seconds_snapshot > 0)',
|
||||||
'CHECK (target_reps_snapshot IS NULL OR target_reps_snapshot > 0)',
|
'CHECK (target_reps_snapshot IS NULL OR target_reps_snapshot > 0)',
|
||||||
'CHECK (target_score_snapshot IS NULL OR target_score_snapshot >= 0)',
|
'CHECK (target_score_snapshot IS NULL OR target_score_snapshot >= 0)',
|
||||||
|
'CHECK (target_score_time_ms_snapshot IS NULL OR '
|
||||||
|
'target_score_time_ms_snapshot > 0)',
|
||||||
'CHECK (actual_time_ms IS NULL OR actual_time_ms >= 0)',
|
'CHECK (actual_time_ms IS NULL OR actual_time_ms >= 0)',
|
||||||
'CHECK (actual_reps IS NULL OR actual_reps >= 0)',
|
'CHECK (actual_reps IS NULL OR actual_reps >= 0)',
|
||||||
'CHECK (actual_score IS NULL OR actual_score >= 0)',
|
'CHECK (actual_score IS NULL OR actual_score >= 0)',
|
||||||
|
'CHECK (actual_score_time_ms IS NULL OR actual_score_time_ms >= 0)',
|
||||||
"CHECK (status IN ('completed', 'skipped'))",
|
"CHECK (status IN ('completed', 'skipped'))",
|
||||||
'CHECK (status != \'skipped\' OR (actual_time_ms IS NULL '
|
'CHECK (status != \'skipped\' OR (actual_time_ms IS NULL '
|
||||||
'AND actual_reps IS NULL AND actual_score IS NULL))',
|
'AND actual_reps IS NULL AND actual_score IS NULL '
|
||||||
|
'AND actual_score_time_ms IS NULL))',
|
||||||
|
"CHECK (score_input_mode_snapshot IN ('manual', 'stopwatch'))",
|
||||||
|
'CHECK (target_score_snapshot IS NULL OR '
|
||||||
|
"(score_enabled_snapshot AND score_input_mode_snapshot = 'manual'))",
|
||||||
|
'CHECK (target_score_time_ms_snapshot IS NULL OR '
|
||||||
|
"(score_enabled_snapshot AND score_input_mode_snapshot = 'stopwatch'))",
|
||||||
|
'CHECK (target_score_snapshot IS NULL OR '
|
||||||
|
'target_score_time_ms_snapshot IS NULL)',
|
||||||
|
'CHECK (actual_score IS NULL OR '
|
||||||
|
"score_input_mode_snapshot = 'manual')",
|
||||||
|
'CHECK (actual_score_time_ms IS NULL OR '
|
||||||
|
"score_input_mode_snapshot = 'stopwatch')",
|
||||||
|
'CHECK (actual_score IS NULL OR actual_score_time_ms IS NULL)',
|
||||||
'CHECK (actual_score IS NULL OR (score_label_snapshot IS NOT NULL '
|
'CHECK (actual_score IS NULL OR (score_label_snapshot IS NOT NULL '
|
||||||
'AND length(trim(score_label_snapshot)) > 0 '
|
'AND length(trim(score_label_snapshot)) > 0 '
|
||||||
'AND score_unit_snapshot IS NOT NULL '
|
'AND score_unit_snapshot IS NOT NULL '
|
||||||
|
|||||||
@ -326,6 +326,124 @@ void main() {
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'score stopwatch start stop resume accumulates elapsed duration',
|
||||||
|
() async {
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final session = _sessionWithSnapshot(
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
);
|
||||||
|
final repository = _FakeActiveSessionRepository()..session = session;
|
||||||
|
final useCase = _activeUseCase(repository, clock);
|
||||||
|
|
||||||
|
await useCase.startScoreStopwatch(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
clock.value = clock.value.add(const Duration(seconds: 5));
|
||||||
|
final stopped = await useCase.stopScoreStopwatch(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
clock.value = clock.value.add(const Duration(seconds: 10));
|
||||||
|
await useCase.resumeScoreStopwatch(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
clock.value = clock.value.add(const Duration(seconds: 3));
|
||||||
|
final stoppedAgain = await useCase.stopScoreStopwatch(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(stopped.accumulatedMs, 5000);
|
||||||
|
expect(stoppedAgain.accumulatedMs, 8000);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('score stopwatch state survives use case reconstruction', () async {
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final session = _sessionWithSnapshot(
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
);
|
||||||
|
final repository = _FakeActiveSessionRepository()..session = session;
|
||||||
|
|
||||||
|
await _activeUseCase(repository, clock).startScoreStopwatch(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
clock.value = clock.value.add(const Duration(seconds: 4));
|
||||||
|
|
||||||
|
final reconstructed = _activeUseCase(repository, clock);
|
||||||
|
final stopped = await reconstructed.stopScoreStopwatch(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(stopped.accumulatedMs, 4000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('score result enforces manual xor stopwatch values', () {
|
||||||
|
expect(
|
||||||
|
() => ActiveSetResult(
|
||||||
|
metadata: _metadata('result-1'),
|
||||||
|
activeWorkoutSessionId: 'session-1',
|
||||||
|
programSnapshotId: 'program-1',
|
||||||
|
exerciseSnapshotId: 'exercise-1',
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
actualScore: 12,
|
||||||
|
actualScoreTimeMs: 12000,
|
||||||
|
),
|
||||||
|
throwsA(isA<DomainException>()),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
() => ActiveSetResult(
|
||||||
|
metadata: _metadata('result-2'),
|
||||||
|
activeWorkoutSessionId: 'session-1',
|
||||||
|
programSnapshotId: 'program-1',
|
||||||
|
exerciseSnapshotId: 'exercise-1',
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
actualScore: 12,
|
||||||
|
scoreInputModeSnapshot: ScoreInputMode.stopwatch,
|
||||||
|
),
|
||||||
|
throwsA(isA<DomainException>()),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
() => ActiveSetResult(
|
||||||
|
metadata: _metadata('result-3'),
|
||||||
|
activeWorkoutSessionId: 'session-1',
|
||||||
|
programSnapshotId: 'program-1',
|
||||||
|
exerciseSnapshotId: 'exercise-1',
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
actualScoreTimeMs: 12000,
|
||||||
|
scoreInputModeSnapshot: ScoreInputMode.manual,
|
||||||
|
),
|
||||||
|
throwsA(isA<DomainException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
EntityMetadata _metadata(String id) {
|
EntityMetadata _metadata(String id) {
|
||||||
@ -414,6 +532,7 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
ActiveWorkoutSession? session;
|
ActiveWorkoutSession? session;
|
||||||
final results = <ActiveSetResult>[];
|
final results = <ActiveSetResult>[];
|
||||||
final restStates = <String, ActiveRestState>{};
|
final restStates = <String, ActiveRestState>{};
|
||||||
|
final scoreStopwatchStates = <String, ActiveScoreStopwatchState>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ActiveWorkoutSession?> findById(String id) async {
|
Future<ActiveWorkoutSession?> findById(String id) async {
|
||||||
@ -426,6 +545,25 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
@override
|
@override
|
||||||
Future<ActiveRestState?> findRestStateById(String id) async => restStates[id];
|
Future<ActiveRestState?> findRestStateById(String id) async => restStates[id];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
}) async {
|
||||||
|
return scoreStopwatchStates.values
|
||||||
|
.where(
|
||||||
|
(state) =>
|
||||||
|
state.activeWorkoutSessionId == sessionId &&
|
||||||
|
state.programIndex == programIndex &&
|
||||||
|
state.exerciseIndex == exerciseIndex &&
|
||||||
|
state.setIndex == setIndex &&
|
||||||
|
state.metadata.deletedAt == null,
|
||||||
|
)
|
||||||
|
.firstOrNull;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||||
return restStates.values
|
return restStates.values
|
||||||
@ -433,6 +571,19 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
|
return scoreStopwatchStates.values
|
||||||
|
.where(
|
||||||
|
(state) =>
|
||||||
|
state.activeWorkoutSessionId == sessionId &&
|
||||||
|
state.metadata.deletedAt == null,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||||||
return results
|
return results
|
||||||
@ -450,6 +601,31 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
restStates[restState.metadata.id] = restState;
|
restStates[restState.metadata.id] = restState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {
|
||||||
|
scoreStopwatchStates[state.metadata.id] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteScoreStopwatchState({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
required DateTime deletedAt,
|
||||||
|
}) async {
|
||||||
|
final state = await findScoreStopwatchState(
|
||||||
|
sessionId: sessionId,
|
||||||
|
programIndex: programIndex,
|
||||||
|
exerciseIndex: exerciseIndex,
|
||||||
|
setIndex: setIndex,
|
||||||
|
);
|
||||||
|
if (state == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scoreStopwatchStates.remove(state.metadata.id);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> saveSetResult(ActiveSetResult result) async {
|
Future<void> saveSetResult(ActiveSetResult result) async {
|
||||||
results.removeWhere(
|
results.removeWhere(
|
||||||
|
|||||||
@ -287,11 +287,26 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
@override
|
@override
|
||||||
Future<ActiveRestState?> findRestStateById(String id) async => null;
|
Future<ActiveRestState?> findRestStateById(String id) async => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
}) async => null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
|
return const [];
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||||||
return const [];
|
return const [];
|
||||||
@ -303,6 +318,18 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
@override
|
@override
|
||||||
Future<void> saveRestState(ActiveRestState restState) async {}
|
Future<void> saveRestState(ActiveRestState restState) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteScoreStopwatchState({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
required DateTime deletedAt,
|
||||||
|
}) async {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> saveSetResult(ActiveSetResult result) async {}
|
Future<void> saveSetResult(ActiveSetResult result) async {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -545,6 +545,7 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
ActiveWorkoutSession? session;
|
ActiveWorkoutSession? session;
|
||||||
final results = <ActiveSetResult>[];
|
final results = <ActiveSetResult>[];
|
||||||
final restStates = <ActiveRestState>[];
|
final restStates = <ActiveRestState>[];
|
||||||
|
final scoreStopwatchStates = <ActiveScoreStopwatchState>[];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<ActiveWorkoutSession?> findById(String id) async {
|
Future<ActiveWorkoutSession?> findById(String id) async {
|
||||||
@ -564,6 +565,25 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<ActiveScoreStopwatchState?> findScoreStopwatchState({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
}) async {
|
||||||
|
for (final state in scoreStopwatchStates) {
|
||||||
|
if (state.activeWorkoutSessionId == sessionId &&
|
||||||
|
state.programIndex == programIndex &&
|
||||||
|
state.exerciseIndex == exerciseIndex &&
|
||||||
|
state.setIndex == setIndex &&
|
||||||
|
state.metadata.deletedAt == null) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
Future<List<ActiveRestState>> listRestStates(String sessionId) async {
|
||||||
return restStates
|
return restStates
|
||||||
@ -571,6 +591,15 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<ActiveScoreStopwatchState>> listScoreStopwatchStates(
|
||||||
|
String sessionId,
|
||||||
|
) async {
|
||||||
|
return scoreStopwatchStates
|
||||||
|
.where((state) => state.activeWorkoutSessionId == sessionId)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
|
||||||
return results;
|
return results;
|
||||||
@ -593,6 +622,27 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveScoreStopwatchState(ActiveScoreStopwatchState state) async {
|
||||||
|
final index = scoreStopwatchStates.indexWhere(
|
||||||
|
(saved) => saved.metadata.id == state.metadata.id,
|
||||||
|
);
|
||||||
|
if (index == -1) {
|
||||||
|
scoreStopwatchStates.add(state);
|
||||||
|
} else {
|
||||||
|
scoreStopwatchStates[index] = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteScoreStopwatchState({
|
||||||
|
required String sessionId,
|
||||||
|
required int programIndex,
|
||||||
|
required int exerciseIndex,
|
||||||
|
required int setIndex,
|
||||||
|
required DateTime deletedAt,
|
||||||
|
}) async {}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> saveSetResult(ActiveSetResult result) async {
|
Future<void> saveSetResult(ActiveSetResult result) async {
|
||||||
final index = results.indexWhere(
|
final index = results.indexWhere(
|
||||||
|
|||||||
Reference in New Issue
Block a user