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 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 save(Exercise exercise) => repository.save(exercise); Future 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 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 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 addExercise({ required String programId, required String exerciseId, required int position, required int setsCount, required Set 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 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 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 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 overrideExerciseTargets({ required String workoutTemplateProgramId, required String snapshotProgramExerciseId, int? setsCountOverride, int? targetTimeSecondsOverride, int? targetRepsOverride, double? targetScoreOverride, Set? 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 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 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 pause(String sessionId) async { final session = await _requiredSession(sessionId); final paused = session.pause(clock.now()); await sessionRepository.save(paused); return paused; } Future 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 recordSetResult(ActiveSetResult result) async { await sessionRepository.saveSetResult(result); return result; } Future 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 _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 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; } } EntityMetadata _newMetadata( IdGenerator ids, String originDeviceId, DateTime now, ) { return EntityMetadata( id: ids.newId(), createdAt: now, updatedAt: now, originDeviceId: originDeviceId, ); }