Files
GameTime/lib/application/use_cases.dart
Blomios 34b3638ce3 feat(ui): écran Historique des séances
Ajoute l'écran d'historique des séances (presentation/history_screen.dart),
avec les ajustements application/domaine/repositories nécessaires et la
navigation depuis l'accueil. Complète le dernier écran du périmètre UI.
flutter analyze propre, 18/18 tests verts, build APK debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 19:17:55 +02:00

901 lines
28 KiB
Dart

import 'dart:convert';
import '../domain/domain.dart';
import 'ports.dart';
final class ExerciseUseCases {
const ExerciseUseCases({
required this.repository,
required this.clock,
required this.ids,
required this.originDeviceId,
});
final ExerciseRepository repository;
final Clock clock;
final IdGenerator ids;
final String originDeviceId;
Future<Exercise> create({
required String name,
String? description,
String? imageMediaId,
String? videoMediaId,
required bool hasTimeMeasure,
required bool hasRepsMeasure,
required bool hasScoreMeasure,
String? scoreLabel,
String? scoreUnit,
}) async {
final now = clock.now();
final exercise = Exercise(
metadata: _newMetadata(ids, originDeviceId, now),
name: name,
description: description,
imageMediaId: imageMediaId,
videoMediaId: videoMediaId,
hasTimeMeasure: hasTimeMeasure,
hasRepsMeasure: hasRepsMeasure,
hasScoreMeasure: hasScoreMeasure,
scoreLabel: scoreLabel,
scoreUnit: scoreUnit,
);
await repository.save(exercise);
return exercise;
}
Future<void> save(Exercise exercise) => repository.save(exercise);
Future<List<Exercise>> listActive() => repository.listActive();
Future<bool> isUsedInPrograms(String id) {
return repository.isReferencedByProgram(id);
}
Future<Exercise> update({
required String id,
required String name,
String? description,
String? imageMediaId,
String? videoMediaId,
required bool hasTimeMeasure,
required bool hasRepsMeasure,
required bool hasScoreMeasure,
String? scoreLabel,
String? scoreUnit,
}) async {
final exercise = await repository.findById(id);
if (exercise == null) {
throw const DomainException('Exercise not found.');
}
final updated = exercise.copyWith(
metadata: exercise.metadata.touch(clock.now()),
name: name,
description: description,
imageMediaId: imageMediaId,
videoMediaId: videoMediaId,
hasTimeMeasure: hasTimeMeasure,
hasRepsMeasure: hasRepsMeasure,
hasScoreMeasure: hasScoreMeasure,
scoreLabel: scoreLabel,
scoreUnit: scoreUnit,
);
await repository.save(updated);
return updated;
}
Future<Exercise> attachImage({
required String exerciseId,
required String mediaAssetId,
}) async {
final exercise = await repository.findById(exerciseId);
if (exercise == null) {
throw const DomainException('Exercise not found.');
}
final updated = exercise.copyWith(
imageMediaId: mediaAssetId,
metadata: exercise.metadata.touch(clock.now()),
);
await repository.save(updated);
return updated;
}
Future<Exercise> attachVideo({
required String exerciseId,
required String mediaAssetId,
}) async {
final exercise = await repository.findById(exerciseId);
if (exercise == null) {
throw const DomainException('Exercise not found.');
}
final updated = exercise.copyWith(
videoMediaId: mediaAssetId,
metadata: exercise.metadata.touch(clock.now()),
);
await repository.save(updated);
return updated;
}
Future<Exercise> archive(String id) async {
final exercise = await repository.findById(id);
if (exercise == null) {
throw const DomainException('Exercise not found.');
}
final archived = exercise.archive(clock.now());
await repository.save(archived);
return archived;
}
}
final class MediaUseCases {
const MediaUseCases({
required this.mediaRepository,
required this.exerciseRepository,
required this.storage,
required this.clock,
required this.ids,
required this.originDeviceId,
});
final MediaAssetRepository mediaRepository;
final ExerciseRepository exerciseRepository;
final LocalMediaStorage storage;
final Clock clock;
final IdGenerator ids;
final String originDeviceId;
Future<MediaAsset> importMedia({
required String sourcePath,
required MediaKind kind,
}) async {
final now = clock.now();
final id = ids.newId();
final stored = await storage.importFile(
sourcePath: sourcePath,
kind: kind,
stableFileName: id,
);
final asset = MediaAsset(
metadata: EntityMetadata(
id: id,
createdAt: now,
updatedAt: now,
originDeviceId: originDeviceId,
),
kind: kind,
localUri: stored.localUri,
mimeType: stored.mimeType,
sizeBytes: stored.sizeBytes,
width: stored.width,
height: stored.height,
durationMs: stored.durationMs,
);
await mediaRepository.save(asset);
return asset;
}
Future<Exercise> attachToExercise({
required String exerciseId,
required String mediaAssetId,
}) async {
final exercise = await exerciseRepository.findById(exerciseId);
if (exercise == null) {
throw const DomainException('Exercise not found.');
}
final media = await mediaRepository.findById(mediaAssetId);
if (media == null) {
throw const DomainException('Media asset not found.');
}
final updated = switch (media.kind) {
MediaKind.image => exercise.copyWith(
imageMediaId: media.metadata.id,
metadata: exercise.metadata.touch(clock.now()),
),
MediaKind.video => exercise.copyWith(
videoMediaId: media.metadata.id,
metadata: exercise.metadata.touch(clock.now()),
),
};
await exerciseRepository.save(updated);
return updated;
}
Future<int> deleteOrphanedManagedFiles() async {
final activeAssets = await mediaRepository.listActive();
final referencedUris = activeAssets.map((asset) => asset.localUri).toSet();
final managedUris = await storage.listManagedLocalUris();
final orphanedUris = managedUris.difference(referencedUris);
for (final uri in orphanedUris) {
await storage.deleteByLocalUri(uri);
}
return orphanedUris.length;
}
}
final class ProgramUseCases {
const ProgramUseCases({
required this.programRepository,
required this.exerciseRepository,
required this.clock,
required this.ids,
required this.originDeviceId,
});
final ProgramRepository programRepository;
final ExerciseRepository exerciseRepository;
final Clock clock;
final IdGenerator ids;
final String originDeviceId;
Future<Program?> findById(String id) => programRepository.findById(id);
Future<List<Program>> listActive() => programRepository.listActive();
Future<Program> create({
required String name,
required int defaultRestSeconds,
}) async {
final now = clock.now();
final program = Program(
metadata: _newMetadata(ids, originDeviceId, now),
name: name,
defaultRestSeconds: defaultRestSeconds,
);
await programRepository.save(program);
return program;
}
Future<Program> saveConfigured({
String? id,
required String name,
required int defaultRestSeconds,
required List<ProgramExerciseConfig> exercises,
}) async {
final now = clock.now();
final existing = id == null ? null : await programRepository.findById(id);
if (id != null && existing == null) {
throw const DomainException('Program not found.');
}
final programId = id ?? ids.newId();
final metadata =
existing?.metadata.touch(now) ??
EntityMetadata(
id: programId,
createdAt: now,
updatedAt: now,
originDeviceId: originDeviceId,
);
final programExercises = <ProgramExercise>[];
for (var index = 0; index < exercises.length; index++) {
final input = exercises[index];
final exerciseMetadata =
input.existingMetadata?.touch(now) ??
_newMetadata(ids, originDeviceId, now);
programExercises.add(
ProgramExercise(
metadata: exerciseMetadata,
programId: programId,
sourceExerciseId: input.sourceExerciseId,
position: index,
exerciseNameSnapshot: input.exerciseNameSnapshot,
exerciseDescriptionSnapshot: input.exerciseDescriptionSnapshot,
exerciseImageMediaIdSnapshot: input.exerciseImageMediaIdSnapshot,
exerciseVideoMediaIdSnapshot: input.exerciseVideoMediaIdSnapshot,
exerciseArchivedSnapshot: input.exerciseArchivedSnapshot,
availableTimeSnapshot: input.availableTimeSnapshot,
availableRepsSnapshot: input.availableRepsSnapshot,
availableScoreSnapshot: input.availableScoreSnapshot,
scoreLabelSnapshot: input.scoreLabelSnapshot,
scoreUnitSnapshot: input.scoreUnitSnapshot,
setsCount: input.setsCount,
timeEnabled: input.enabledMeasures.contains(WorkoutMeasure.time),
repsEnabled: input.enabledMeasures.contains(WorkoutMeasure.reps),
scoreEnabled: input.enabledMeasures.contains(WorkoutMeasure.score),
targetTimeSeconds: input.targetTimeSeconds,
targetReps: input.targetReps,
targetScore: input.targetScore,
restSecondsOverride: input.restSecondsOverride,
),
);
}
final program = Program(
metadata: metadata,
name: name,
defaultRestSeconds: defaultRestSeconds,
exercises: programExercises,
);
await programRepository.replaceExercises(program, now);
return program;
}
Future<ProgramExercise> addExercise({
required String programId,
required String exerciseId,
required int position,
required int setsCount,
required Set<WorkoutMeasure> enabledMeasures,
int? targetTimeSeconds,
int? targetReps,
double? targetScore,
int? restSecondsOverride,
}) async {
final source = await exerciseRepository.findById(exerciseId);
if (source == null) {
throw const DomainException('Exercise not found.');
}
final now = clock.now();
final programExercise = ProgramExercise.snapshotFromExercise(
metadata: _newMetadata(ids, originDeviceId, now),
programId: programId,
exercise: source,
position: position,
setsCount: setsCount,
enabledMeasures: enabledMeasures,
targetTimeSeconds: targetTimeSeconds,
targetReps: targetReps,
targetScore: targetScore,
restSecondsOverride: restSecondsOverride,
);
await programRepository.saveExercise(programExercise);
return programExercise;
}
}
final class ProgramExerciseConfig {
const ProgramExerciseConfig({
this.existingMetadata,
this.sourceExerciseId,
required this.exerciseNameSnapshot,
this.exerciseDescriptionSnapshot,
this.exerciseImageMediaIdSnapshot,
this.exerciseVideoMediaIdSnapshot,
this.exerciseArchivedSnapshot = false,
required this.availableTimeSnapshot,
required this.availableRepsSnapshot,
required this.availableScoreSnapshot,
this.scoreLabelSnapshot,
this.scoreUnitSnapshot,
required this.setsCount,
required this.enabledMeasures,
this.targetTimeSeconds,
this.targetReps,
this.targetScore,
this.restSecondsOverride,
});
final EntityMetadata? existingMetadata;
final String? sourceExerciseId;
final String exerciseNameSnapshot;
final String? exerciseDescriptionSnapshot;
final String? exerciseImageMediaIdSnapshot;
final String? exerciseVideoMediaIdSnapshot;
final bool exerciseArchivedSnapshot;
final bool availableTimeSnapshot;
final bool availableRepsSnapshot;
final bool availableScoreSnapshot;
final String? scoreLabelSnapshot;
final String? scoreUnitSnapshot;
final int setsCount;
final Set<WorkoutMeasure> enabledMeasures;
final int? targetTimeSeconds;
final int? targetReps;
final double? targetScore;
final int? restSecondsOverride;
}
final class WorkoutTemplateUseCases {
const WorkoutTemplateUseCases({
required this.templateRepository,
required this.programRepository,
required this.clock,
required this.ids,
required this.originDeviceId,
});
final WorkoutTemplateRepository templateRepository;
final ProgramRepository programRepository;
final Clock clock;
final IdGenerator ids;
final String originDeviceId;
Future<WorkoutTemplate?> findById(String id) {
return templateRepository.findById(id);
}
Future<List<WorkoutTemplate>> listActive() {
return templateRepository.listActive();
}
Future<WorkoutTemplate> create({required String name}) async {
final now = clock.now();
final template = WorkoutTemplate(
metadata: _newMetadata(ids, originDeviceId, now),
name: name,
);
await templateRepository.save(template);
return template;
}
Future<WorkoutTemplate> saveConfigured({
String? id,
required String name,
required List<WorkoutTemplateProgramConfig> programs,
required List<WorkoutTemplateExerciseOverrideConfig> overrides,
}) async {
final now = clock.now();
final existing = id == null ? null : await templateRepository.findById(id);
if (id != null && existing == null) {
throw const DomainException('Workout template not found.');
}
final templateId = id ?? ids.newId();
final metadata =
existing?.metadata.touch(now) ??
EntityMetadata(
id: templateId,
createdAt: now,
updatedAt: now,
originDeviceId: originDeviceId,
);
final templatePrograms = <WorkoutTemplateProgram>[];
final programIdsByClientKey = <String, String>{};
for (var index = 0; index < programs.length; index++) {
final input = programs[index];
final programMetadata =
input.existingMetadata?.touch(now) ??
_newMetadata(ids, originDeviceId, now);
programIdsByClientKey[input.clientKey] = programMetadata.id;
templatePrograms.add(
WorkoutTemplateProgram(
metadata: programMetadata,
workoutTemplateId: templateId,
sourceProgramId: input.sourceProgramId,
position: index,
programNameSnapshot: input.programNameSnapshot,
defaultRestSecondsSnapshot: input.defaultRestSecondsSnapshot,
programSnapshotJson: input.programSnapshotJson,
),
);
}
final templateOverrides = <WorkoutTemplateExerciseOverride>[];
for (final input in overrides) {
final workoutTemplateProgramId =
programIdsByClientKey[input.workoutTemplateProgramClientKey];
if (workoutTemplateProgramId == null) {
throw const DomainException('Workout template program not found.');
}
templateOverrides.add(
WorkoutTemplateExerciseOverride(
metadata:
input.existingMetadata?.touch(now) ??
_newMetadata(ids, originDeviceId, now),
workoutTemplateProgramId: workoutTemplateProgramId,
snapshotProgramExerciseId: input.snapshotProgramExerciseId,
setsCountOverride: input.setsCountOverride,
targetTimeSecondsOverride: input.targetTimeSecondsOverride,
targetRepsOverride: input.targetRepsOverride,
targetScoreOverride: input.targetScoreOverride,
),
);
}
final template = WorkoutTemplate(
metadata: metadata,
name: name,
lastStartedAt: existing?.lastStartedAt,
programs: templatePrograms,
overrides: templateOverrides,
);
await templateRepository.replaceComposition(template, now);
return template;
}
Future<WorkoutTemplateProgram> addProgramSnapshot({
required String workoutTemplateId,
required String programId,
required int position,
}) async {
final program = await programRepository.findById(programId);
if (program == null) {
throw const DomainException('Program not found.');
}
final now = clock.now();
final snapshot = WorkoutTemplateProgram.snapshotFromProgram(
metadata: _newMetadata(ids, originDeviceId, now),
workoutTemplateId: workoutTemplateId,
program: program,
position: position,
);
await templateRepository.saveProgram(snapshot);
return snapshot;
}
Future<WorkoutTemplateExerciseOverride> overrideExerciseTargets({
required String workoutTemplateProgramId,
required String snapshotProgramExerciseId,
int? setsCountOverride,
int? targetTimeSecondsOverride,
int? targetRepsOverride,
double? targetScoreOverride,
Set<WorkoutMeasure>? attemptedMeasureOverride,
}) async {
if (attemptedMeasureOverride != null) {
throw const DomainException(
'Workout templates cannot override active measures.',
);
}
final now = clock.now();
final override = WorkoutTemplateExerciseOverride(
metadata: _newMetadata(ids, originDeviceId, now),
workoutTemplateProgramId: workoutTemplateProgramId,
snapshotProgramExerciseId: snapshotProgramExerciseId,
setsCountOverride: setsCountOverride,
targetTimeSecondsOverride: targetTimeSecondsOverride,
targetRepsOverride: targetRepsOverride,
targetScoreOverride: targetScoreOverride,
);
await templateRepository.saveOverride(override);
return override;
}
}
final class WorkoutTemplateProgramConfig {
const WorkoutTemplateProgramConfig({
required this.clientKey,
this.existingMetadata,
this.sourceProgramId,
required this.programNameSnapshot,
required this.defaultRestSecondsSnapshot,
required this.programSnapshotJson,
});
final String clientKey;
final EntityMetadata? existingMetadata;
final String? sourceProgramId;
final String programNameSnapshot;
final int defaultRestSecondsSnapshot;
final String programSnapshotJson;
}
final class WorkoutTemplateExerciseOverrideConfig {
const WorkoutTemplateExerciseOverrideConfig({
this.existingMetadata,
required this.workoutTemplateProgramClientKey,
required this.snapshotProgramExerciseId,
this.setsCountOverride,
this.targetTimeSecondsOverride,
this.targetRepsOverride,
this.targetScoreOverride,
});
final EntityMetadata? existingMetadata;
final String workoutTemplateProgramClientKey;
final String snapshotProgramExerciseId;
final int? setsCountOverride;
final int? targetTimeSecondsOverride;
final int? targetRepsOverride;
final double? targetScoreOverride;
}
final class ActiveWorkoutSessionUseCases {
const ActiveWorkoutSessionUseCases({
required this.sessionRepository,
required this.templateRepository,
required this.clock,
required this.ids,
required this.originDeviceId,
});
final ActiveSessionRepository sessionRepository;
final WorkoutTemplateRepository templateRepository;
final Clock clock;
final IdGenerator ids;
final String originDeviceId;
Future<ActiveWorkoutSession?> findOpen() => sessionRepository.findOpen();
Future<ActiveWorkoutSession> startFromTemplate(String templateId) async {
final template = await templateRepository.findById(templateId);
if (template == null) {
throw const DomainException('Workout template not found.');
}
final now = clock.now();
final session = ActiveWorkoutSession(
metadata: _newMetadata(ids, originDeviceId, now),
sourceWorkoutTemplateId: templateId,
status: ActiveWorkoutStatus.running,
startedAt: now,
lastPersistedAt: now,
elapsedActiveMs: 0,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: jsonEncode({
'templateId': template.metadata.id,
'name': template.name,
'programs': template.programs
.map(
(program) => {
'id': program.metadata.id,
'programNameSnapshot': program.programNameSnapshot,
'programSnapshotJson': program.programSnapshotJson,
},
)
.toList(),
'overrides': template.overrides
.map(
(override) => {
'workoutTemplateProgramId': override.workoutTemplateProgramId,
'snapshotProgramExerciseId': override.snapshotProgramExerciseId,
'setsCountOverride': override.setsCountOverride,
'targetTimeSecondsOverride': override.targetTimeSecondsOverride,
'targetRepsOverride': override.targetRepsOverride,
'targetScoreOverride': override.targetScoreOverride,
},
)
.toList(),
}),
);
await sessionRepository.save(session);
return session;
}
Future<ActiveWorkoutSession> startFromHistory(WorkoutHistory history) async {
final snapshot =
jsonDecode(history.historySnapshotJson) as Map<String, dynamic>;
final resolvedTemplateSnapshotJson =
snapshot['resolvedTemplateSnapshotJson'] as String;
final now = clock.now();
final session = ActiveWorkoutSession(
metadata: _newMetadata(ids, originDeviceId, now),
sourceWorkoutTemplateId: null,
status: ActiveWorkoutStatus.running,
startedAt: now,
lastPersistedAt: now,
elapsedActiveMs: 0,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: resolvedTemplateSnapshotJson,
);
await sessionRepository.save(session);
return session;
}
Future<ActiveWorkoutSession> pause(String sessionId) async {
final session = await _requiredSession(sessionId);
final paused = session.pause(clock.now());
await sessionRepository.save(paused);
return paused;
}
Future<ActiveWorkoutSession> resume(String sessionId) async {
final session = await _requiredSession(sessionId);
final resumed = session.resume(clock.now());
await sessionRepository.save(resumed);
return resumed;
}
int elapsedActiveMilliseconds(ActiveWorkoutSession session) {
return session.elapsedActiveMillisecondsAt(clock.now());
}
Future<ActiveSetResult> recordSetResult(ActiveSetResult result) async {
await sessionRepository.saveSetResult(result);
return result;
}
Future<ActiveSetResult> recordCurrentSetResult({
required String sessionId,
required String programSnapshotId,
required String exerciseSnapshotId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
int? actualTimeMs,
int? actualReps,
double? actualScore,
String? scoreLabelSnapshot,
String? scoreUnitSnapshot,
}) async {
final now = clock.now();
final result = ActiveSetResult(
metadata: _newMetadata(ids, originDeviceId, now),
activeWorkoutSessionId: sessionId,
programSnapshotId: programSnapshotId,
exerciseSnapshotId: exerciseSnapshotId,
programIndex: programIndex,
exerciseIndex: exerciseIndex,
setIndex: setIndex,
completedAt: now,
actualTimeMs: actualTimeMs,
actualReps: actualReps,
actualScore: actualScore,
scoreLabelSnapshot: scoreLabelSnapshot,
scoreUnitSnapshot: scoreUnitSnapshot,
);
await sessionRepository.saveSetResult(result);
return result;
}
Future<ActiveWorkoutSession> updateProgress({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
}) async {
final session = await _requiredSession(sessionId);
final updated = session.copyWith(
currentProgramIndex: programIndex,
currentExerciseIndex: exerciseIndex,
currentSetIndex: setIndex,
metadata: session.metadata.touch(clock.now()),
);
await sessionRepository.save(updated);
return updated;
}
Future<ActiveWorkoutSession> quitAndSave(String sessionId) async {
final session = await _requiredSession(sessionId);
final now = clock.now();
final saved = session.copyWith(
status: ActiveWorkoutStatus.savedExit,
lastPersistedAt: now,
elapsedActiveMs: session.elapsedActiveMillisecondsAt(now),
metadata: session.metadata.touch(now),
);
await sessionRepository.save(saved);
return saved;
}
Future<ActiveWorkoutSession> abandon(String sessionId) async {
final session = await _requiredSession(sessionId);
final now = clock.now();
final abandoned = session.copyWith(
status: ActiveWorkoutStatus.abandoned,
endedAt: now,
lastPersistedAt: now,
elapsedActiveMs: session.elapsedActiveMillisecondsAt(now),
metadata: session.metadata.touch(now),
);
await sessionRepository.save(abandoned);
return abandoned;
}
Future<ActiveWorkoutSession> complete(String sessionId) async {
final session = await _requiredSession(sessionId);
final now = clock.now();
final completed = session.copyWith(
status: ActiveWorkoutStatus.completed,
endedAt: now,
lastPersistedAt: now,
elapsedActiveMs: session.elapsedActiveMillisecondsAt(now),
metadata: session.metadata.touch(now),
);
await sessionRepository.save(completed);
return completed;
}
Future<ActiveRestState> startRestAfterSet({
required String sessionId,
required int afterProgramIndex,
required int afterExerciseIndex,
required int afterSetIndex,
required int plannedRestSeconds,
int? adjustedRestSeconds,
}) async {
final now = clock.now();
final rest = ActiveRestState(
metadata: _newMetadata(ids, originDeviceId, now),
activeWorkoutSessionId: sessionId,
afterProgramIndex: afterProgramIndex,
afterExerciseIndex: afterExerciseIndex,
afterSetIndex: afterSetIndex,
plannedRestSeconds: plannedRestSeconds,
adjustedRestSeconds: adjustedRestSeconds ?? plannedRestSeconds,
startedAt: now,
);
await sessionRepository.saveRestState(rest);
return rest;
}
Future<ActiveWorkoutSession> _requiredSession(String sessionId) async {
final session = await sessionRepository.findById(sessionId);
if (session == null) {
throw const DomainException('Active workout session not found.');
}
return session;
}
}
final class CloseWorkoutSessionUseCase {
const CloseWorkoutSessionUseCase({
required this.sessionRepository,
required this.historyRepository,
required this.clock,
required this.ids,
required this.originDeviceId,
});
final ActiveSessionRepository sessionRepository;
final WorkoutHistoryRepository historyRepository;
final Clock clock;
final IdGenerator ids;
final String originDeviceId;
Future<WorkoutHistory> close({
required String sessionId,
required String nameSnapshot,
required bool completed,
}) async {
final session = await sessionRepository.findById(sessionId);
if (session == null) {
throw const DomainException('Active workout session not found.');
}
final now = clock.now();
final results = await sessionRepository.listSetResults(sessionId);
final historyId = ids.newId();
final history = WorkoutHistory(
metadata: EntityMetadata(
id: historyId,
createdAt: now,
updatedAt: now,
originDeviceId: originDeviceId,
),
sourceWorkoutTemplateId: session.sourceWorkoutTemplateId,
sourceActiveWorkoutSessionId: session.metadata.id,
nameSnapshot: nameSnapshot,
startedAt: session.startedAt,
endedAt: now,
totalActiveMs: session.elapsedActiveMillisecondsAt(now),
completed: completed,
historySnapshotJson: jsonEncode({
'sessionId': session.metadata.id,
'resolvedTemplateSnapshotJson': session.resolvedTemplateSnapshotJson,
'results': results
.map(
(result) => {
'programSnapshotId': result.programSnapshotId,
'exerciseSnapshotId': result.exerciseSnapshotId,
'programIndex': result.programIndex,
'exerciseIndex': result.exerciseIndex,
'setIndex': result.setIndex,
'actualTimeMs': result.actualTimeMs,
'actualReps': result.actualReps,
'actualScore': result.actualScore,
'scoreLabelSnapshot': result.scoreLabelSnapshot,
'scoreUnitSnapshot': result.scoreUnitSnapshot,
},
)
.toList(),
}),
);
await historyRepository.save(history);
return history;
}
}
final class WorkoutHistoryUseCases {
const WorkoutHistoryUseCases({required this.repository, required this.clock});
final WorkoutHistoryRepository repository;
final Clock clock;
Future<WorkoutHistory?> findById(String id) => repository.findById(id);
Future<List<WorkoutHistory>> listActive() => repository.listActive();
Future<void> delete(String id) => repository.delete(id, clock.now());
}
EntityMetadata _newMetadata(
IdGenerator ids,
String originDeviceId,
DateTime now,
) {
return EntityMetadata(
id: ids.newId(),
createdAt: now,
updatedAt: now,
originDeviceId: originDeviceId,
);
}