merge(develop): intègre la couche application (ticket #4)
Fusionne feature/#4-usecases-repositories — analyze propre, tests unitaires verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#4"
|
issueRef: "#4"
|
||||||
version: 3
|
version: 5
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784301378940
|
updatedAt: 1784305595841
|
||||||
---
|
---
|
||||||
Règle produit validée avec l'utilisateur (mémoire "gametime-ux-conception" section 3) : une séance-modèle peut surcharger uniquement le nombre de séries et les valeurs cibles numériques des mesures déjà actives d'un exercice — jamais quelles mesures sont actives, jamais l'ordre/la liste des exercices. Ce garde-fou doit être appliqué au niveau des use cases, pas seulement de l'UI.
|
Règle produit validée avec l'utilisateur (mémoire "gametime-ux-conception" section 3) : une séance-modèle peut surcharger uniquement le nombre de séries et les valeurs cibles numériques des mesures déjà actives d'un exercice — jamais quelles mesures sont actives, jamais l'ordre/la liste des exercices. Ce garde-fou doit être appliqué au niveau des use cases, pas seulement de l'UI.
|
||||||
Pause/reprise de session : recalculer les temps écoulés depuis les horodatages stockés (startedAt, pausedAt, lastPersistedAt), jamais depuis un compteur en mémoire — nécessaire pour survivre à une fermeture d'app.
|
Pause/reprise de session : recalculer les temps écoulés depuis les horodatages stockés (startedAt, pausedAt, lastPersistedAt), jamais depuis un compteur en mémoire — nécessaire pour survivre à une fermeture d'app.
|
||||||
@ -2,7 +2,7 @@
|
|||||||
id: "d3a8bc95-d1c7-4114-a964-7faa5b15835a"
|
id: "d3a8bc95-d1c7-4114-a964-7faa5b15835a"
|
||||||
number: 4
|
number: 4
|
||||||
title: "[DevBackend] Couche application : use cases et repositories"
|
title: "[DevBackend] Couche application : use cases et repositories"
|
||||||
status: "open"
|
status: "closed"
|
||||||
priority: "high"
|
priority: "high"
|
||||||
sprint: null
|
sprint: null
|
||||||
links: [{"target":"#3","kind":"dependsOn"}]
|
links: [{"target":"#3","kind":"dependsOn"}]
|
||||||
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"10ee045b-1c41-479e-ba03-dceed9edd495","role":"assigned"}
|
|||||||
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784301293308
|
createdAt: 1784301293308
|
||||||
updatedAt: 1784301378940
|
updatedAt: 1784305595841
|
||||||
version: 3
|
version: 5
|
||||||
---
|
---
|
||||||
Implémenter ports/repositories et use cases pour : CRUD Exercise (règle "au moins une mesure active", avertissement si exercice déjà utilisé, archivage doux plutôt que suppression) ; CRUD Program (snapshot de l'exercice à l'ajout, mesures activées parmi celles autorisées, séries, cibles, repos rattaché à la série/l'exercice précédent) ; gestion WorkoutTemplate (composition de programmes en snapshot indépendant, overrides limités à setsCount et valeurs cibles numériques uniquement — pas de changement de mesures actives ni de structure) ; gestion ActiveWorkoutSession (démarrage, transitions série/repos, pause/reprise basées sur horodatages, pas sur un compteur mémoire) ; clôture de séance vers WorkoutHistory (snapshot autonome).
|
Implémenter ports/repositories et use cases pour : CRUD Exercise (règle "au moins une mesure active", avertissement si exercice déjà utilisé, archivage doux plutôt que suppression) ; CRUD Program (snapshot de l'exercice à l'ajout, mesures activées parmi celles autorisées, séries, cibles, repos rattaché à la série/l'exercice précédent) ; gestion WorkoutTemplate (composition de programmes en snapshot indépendant, overrides limités à setsCount et valeurs cibles numériques uniquement — pas de changement de mesures actives ni de structure) ; gestion ActiveWorkoutSession (démarrage, transitions série/repos, pause/reprise basées sur horodatages, pas sur un compteur mémoire) ; clôture de séance vers WorkoutHistory (snapshot autonome).
|
||||||
@ -41,13 +41,13 @@
|
|||||||
"issueRef": "#4",
|
"issueRef": "#4",
|
||||||
"path": "4",
|
"path": "4",
|
||||||
"title": "[DevBackend] Couche application : use cases et repositories",
|
"title": "[DevBackend] Couche application : use cases et repositories",
|
||||||
"status": "open",
|
"status": "closed",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"sprint": null,
|
"sprint": null,
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"10ee045b-1c41-479e-ba03-dceed9edd495"
|
"10ee045b-1c41-479e-ba03-dceed9edd495"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784301378940
|
"updatedAt": 1784305595841
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#5",
|
"issueRef": "#5",
|
||||||
|
|||||||
@ -3,3 +3,6 @@
|
|||||||
/// This layer owns use cases and ports. It may depend on domain, but concrete
|
/// This layer owns use cases and ports. It may depend on domain, but concrete
|
||||||
/// adapters are wired only from the composition root.
|
/// adapters are wired only from the composition root.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
export 'ports.dart';
|
||||||
|
export 'use_cases.dart';
|
||||||
|
|||||||
49
lib/application/ports.dart
Normal file
49
lib/application/ports.dart
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import '../domain/domain.dart';
|
||||||
|
|
||||||
|
abstract interface class Clock {
|
||||||
|
DateTime now();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SystemClock implements Clock {
|
||||||
|
const SystemClock();
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime now() => DateTime.now().toUtc();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface class IdGenerator {
|
||||||
|
String newId();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface class ExerciseRepository {
|
||||||
|
Future<Exercise?> findById(String id);
|
||||||
|
Future<List<Exercise>> listActive();
|
||||||
|
Future<void> save(Exercise exercise);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface class ProgramRepository {
|
||||||
|
Future<Program?> findById(String id);
|
||||||
|
Future<void> save(Program program);
|
||||||
|
Future<void> saveExercise(ProgramExercise exercise);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface class WorkoutTemplateRepository {
|
||||||
|
Future<WorkoutTemplate?> findById(String id);
|
||||||
|
Future<void> save(WorkoutTemplate template);
|
||||||
|
Future<void> saveProgram(WorkoutTemplateProgram program);
|
||||||
|
Future<void> saveOverride(WorkoutTemplateExerciseOverride override);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface class ActiveSessionRepository {
|
||||||
|
Future<ActiveWorkoutSession?> findById(String id);
|
||||||
|
Future<void> save(ActiveWorkoutSession session);
|
||||||
|
Future<void> saveSetResult(ActiveSetResult result);
|
||||||
|
Future<void> saveRestState(ActiveRestState restState);
|
||||||
|
Future<List<ActiveSetResult>> listSetResults(String sessionId);
|
||||||
|
Future<List<ActiveRestState>> listRestStates(String sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface class WorkoutHistoryRepository {
|
||||||
|
Future<void> save(WorkoutHistory history);
|
||||||
|
Future<void> saveSetResult(WorkoutHistorySetResult result);
|
||||||
|
}
|
||||||
390
lib/application/use_cases.dart
Normal file
390
lib/application/use_cases.dart
Normal file
@ -0,0 +1,390 @@
|
|||||||
|
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<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 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> 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<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 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> 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<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 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> 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> 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<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityMetadata _newMetadata(
|
||||||
|
IdGenerator ids,
|
||||||
|
String originDeviceId,
|
||||||
|
DateTime now,
|
||||||
|
) {
|
||||||
|
return EntityMetadata(
|
||||||
|
id: ids.newId(),
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
originDeviceId: originDeviceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,3 +2,5 @@
|
|||||||
///
|
///
|
||||||
/// Keep this layer free from Flutter, Drift, platform APIs, and IO.
|
/// Keep this layer free from Flutter, Drift, platform APIs, and IO.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
export 'entities.dart';
|
||||||
|
|||||||
742
lib/domain/entities.dart
Normal file
742
lib/domain/entities.dart
Normal file
@ -0,0 +1,742 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
enum SyncState { localOnly, dirty, synced, deleted }
|
||||||
|
|
||||||
|
enum MediaKind { image, video }
|
||||||
|
|
||||||
|
enum WorkoutMeasure { time, reps, score }
|
||||||
|
|
||||||
|
enum ActiveWorkoutStatus { running, paused, savedExit, completed, abandoned }
|
||||||
|
|
||||||
|
final class DomainException implements Exception {
|
||||||
|
const DomainException(this.message);
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => message;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class EntityMetadata {
|
||||||
|
const EntityMetadata({
|
||||||
|
required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.originDeviceId,
|
||||||
|
this.deletedAt,
|
||||||
|
this.schemaVersion = 1,
|
||||||
|
this.syncState = SyncState.dirty,
|
||||||
|
this.localRevision = 0,
|
||||||
|
this.futureOwnerProfileId,
|
||||||
|
this.lastSyncedAt,
|
||||||
|
this.remoteRevision,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final DateTime? deletedAt;
|
||||||
|
final int schemaVersion;
|
||||||
|
final SyncState syncState;
|
||||||
|
final int localRevision;
|
||||||
|
final String originDeviceId;
|
||||||
|
final String? futureOwnerProfileId;
|
||||||
|
final DateTime? lastSyncedAt;
|
||||||
|
final String? remoteRevision;
|
||||||
|
|
||||||
|
EntityMetadata touch(DateTime now) {
|
||||||
|
return copyWith(updatedAt: now, localRevision: localRevision + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityMetadata markDeleted(DateTime now) {
|
||||||
|
return copyWith(
|
||||||
|
updatedAt: now,
|
||||||
|
deletedAt: now,
|
||||||
|
syncState: SyncState.deleted,
|
||||||
|
localRevision: localRevision + 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityMetadata copyWith({
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
DateTime? deletedAt,
|
||||||
|
int? schemaVersion,
|
||||||
|
SyncState? syncState,
|
||||||
|
int? localRevision,
|
||||||
|
String? originDeviceId,
|
||||||
|
String? futureOwnerProfileId,
|
||||||
|
DateTime? lastSyncedAt,
|
||||||
|
String? remoteRevision,
|
||||||
|
}) {
|
||||||
|
return EntityMetadata(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
schemaVersion: schemaVersion ?? this.schemaVersion,
|
||||||
|
syncState: syncState ?? this.syncState,
|
||||||
|
localRevision: localRevision ?? this.localRevision,
|
||||||
|
originDeviceId: originDeviceId ?? this.originDeviceId,
|
||||||
|
futureOwnerProfileId: futureOwnerProfileId ?? this.futureOwnerProfileId,
|
||||||
|
lastSyncedAt: lastSyncedAt ?? this.lastSyncedAt,
|
||||||
|
remoteRevision: remoteRevision ?? this.remoteRevision,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MediaAsset {
|
||||||
|
const MediaAsset({
|
||||||
|
required this.metadata,
|
||||||
|
required this.kind,
|
||||||
|
required this.localUri,
|
||||||
|
this.mimeType,
|
||||||
|
this.sizeBytes,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.durationMs,
|
||||||
|
this.checksum,
|
||||||
|
this.remoteUri,
|
||||||
|
this.thumbnailLocalUri,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EntityMetadata metadata;
|
||||||
|
final MediaKind kind;
|
||||||
|
final String localUri;
|
||||||
|
final String? mimeType;
|
||||||
|
final int? sizeBytes;
|
||||||
|
final int? width;
|
||||||
|
final int? height;
|
||||||
|
final int? durationMs;
|
||||||
|
final String? checksum;
|
||||||
|
final String? remoteUri;
|
||||||
|
final String? thumbnailLocalUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Exercise {
|
||||||
|
Exercise({
|
||||||
|
required this.metadata,
|
||||||
|
required String name,
|
||||||
|
this.description,
|
||||||
|
this.imageMediaId,
|
||||||
|
this.videoMediaId,
|
||||||
|
required this.hasTimeMeasure,
|
||||||
|
required this.hasRepsMeasure,
|
||||||
|
required this.hasScoreMeasure,
|
||||||
|
this.scoreLabel,
|
||||||
|
this.scoreUnit,
|
||||||
|
this.archivedAt,
|
||||||
|
}) : name = _nonBlank(name, 'Exercise name') {
|
||||||
|
_requireAtLeastOneMeasure(
|
||||||
|
hasTime: hasTimeMeasure,
|
||||||
|
hasReps: hasRepsMeasure,
|
||||||
|
hasScore: hasScoreMeasure,
|
||||||
|
);
|
||||||
|
if (hasScoreMeasure) {
|
||||||
|
_nonBlank(scoreLabel, 'Score label');
|
||||||
|
_nonBlank(scoreUnit, 'Score unit');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final EntityMetadata metadata;
|
||||||
|
final String name;
|
||||||
|
final String? description;
|
||||||
|
final String? imageMediaId;
|
||||||
|
final String? videoMediaId;
|
||||||
|
final bool hasTimeMeasure;
|
||||||
|
final bool hasRepsMeasure;
|
||||||
|
final bool hasScoreMeasure;
|
||||||
|
final String? scoreLabel;
|
||||||
|
final String? scoreUnit;
|
||||||
|
final DateTime? archivedAt;
|
||||||
|
|
||||||
|
Set<WorkoutMeasure> get availableMeasures => {
|
||||||
|
if (hasTimeMeasure) WorkoutMeasure.time,
|
||||||
|
if (hasRepsMeasure) WorkoutMeasure.reps,
|
||||||
|
if (hasScoreMeasure) WorkoutMeasure.score,
|
||||||
|
};
|
||||||
|
|
||||||
|
Exercise archive(DateTime now) {
|
||||||
|
return copyWith(archivedAt: now, metadata: metadata.touch(now));
|
||||||
|
}
|
||||||
|
|
||||||
|
Exercise copyWith({
|
||||||
|
EntityMetadata? metadata,
|
||||||
|
String? name,
|
||||||
|
String? description,
|
||||||
|
String? imageMediaId,
|
||||||
|
String? videoMediaId,
|
||||||
|
bool? hasTimeMeasure,
|
||||||
|
bool? hasRepsMeasure,
|
||||||
|
bool? hasScoreMeasure,
|
||||||
|
String? scoreLabel,
|
||||||
|
String? scoreUnit,
|
||||||
|
DateTime? archivedAt,
|
||||||
|
}) {
|
||||||
|
return Exercise(
|
||||||
|
metadata: metadata ?? this.metadata,
|
||||||
|
name: name ?? this.name,
|
||||||
|
description: description ?? this.description,
|
||||||
|
imageMediaId: imageMediaId ?? this.imageMediaId,
|
||||||
|
videoMediaId: videoMediaId ?? this.videoMediaId,
|
||||||
|
hasTimeMeasure: hasTimeMeasure ?? this.hasTimeMeasure,
|
||||||
|
hasRepsMeasure: hasRepsMeasure ?? this.hasRepsMeasure,
|
||||||
|
hasScoreMeasure: hasScoreMeasure ?? this.hasScoreMeasure,
|
||||||
|
scoreLabel: scoreLabel ?? this.scoreLabel,
|
||||||
|
scoreUnit: scoreUnit ?? this.scoreUnit,
|
||||||
|
archivedAt: archivedAt ?? this.archivedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Program {
|
||||||
|
Program({
|
||||||
|
required this.metadata,
|
||||||
|
required String name,
|
||||||
|
required this.defaultRestSeconds,
|
||||||
|
this.exercises = const [],
|
||||||
|
}) : name = _nonBlank(name, 'Program name') {
|
||||||
|
_requireNonNegative(defaultRestSeconds, 'Default rest seconds');
|
||||||
|
}
|
||||||
|
|
||||||
|
final EntityMetadata metadata;
|
||||||
|
final String name;
|
||||||
|
final int defaultRestSeconds;
|
||||||
|
final List<ProgramExercise> exercises;
|
||||||
|
|
||||||
|
Program copyWith({
|
||||||
|
List<ProgramExercise>? exercises,
|
||||||
|
EntityMetadata? metadata,
|
||||||
|
}) {
|
||||||
|
return Program(
|
||||||
|
metadata: metadata ?? this.metadata,
|
||||||
|
name: name,
|
||||||
|
defaultRestSeconds: defaultRestSeconds,
|
||||||
|
exercises: exercises ?? this.exercises,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ProgramExercise {
|
||||||
|
ProgramExercise({
|
||||||
|
required this.metadata,
|
||||||
|
required this.programId,
|
||||||
|
this.sourceExerciseId,
|
||||||
|
required this.position,
|
||||||
|
required String 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.timeEnabled,
|
||||||
|
required this.repsEnabled,
|
||||||
|
required this.scoreEnabled,
|
||||||
|
this.targetTimeSeconds,
|
||||||
|
this.targetReps,
|
||||||
|
this.targetScore,
|
||||||
|
this.restSecondsOverride,
|
||||||
|
}) : exerciseNameSnapshot = _nonBlank(
|
||||||
|
exerciseNameSnapshot,
|
||||||
|
'Exercise snapshot name',
|
||||||
|
) {
|
||||||
|
_requireNonNegative(position, 'Position');
|
||||||
|
_requirePositive(setsCount, 'Sets count');
|
||||||
|
_requireAtLeastOneMeasure(
|
||||||
|
hasTime: timeEnabled,
|
||||||
|
hasReps: repsEnabled,
|
||||||
|
hasScore: scoreEnabled,
|
||||||
|
);
|
||||||
|
if (timeEnabled && !availableTimeSnapshot) {
|
||||||
|
throw const DomainException('Time is not available on this exercise.');
|
||||||
|
}
|
||||||
|
if (repsEnabled && !availableRepsSnapshot) {
|
||||||
|
throw const DomainException('Reps are not available on this exercise.');
|
||||||
|
}
|
||||||
|
if (scoreEnabled && !availableScoreSnapshot) {
|
||||||
|
throw const DomainException('Score is not available on this exercise.');
|
||||||
|
}
|
||||||
|
_requireNullablePositive(targetTimeSeconds, 'Target time seconds');
|
||||||
|
_requireNullablePositive(targetReps, 'Target reps');
|
||||||
|
_requireNullableNonNegativeDouble(targetScore, 'Target score');
|
||||||
|
_requireNullableNonNegative(restSecondsOverride, 'Rest seconds override');
|
||||||
|
}
|
||||||
|
|
||||||
|
final EntityMetadata metadata;
|
||||||
|
final String programId;
|
||||||
|
final String? sourceExerciseId;
|
||||||
|
final int position;
|
||||||
|
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 bool timeEnabled;
|
||||||
|
final bool repsEnabled;
|
||||||
|
final bool scoreEnabled;
|
||||||
|
final int? targetTimeSeconds;
|
||||||
|
final int? targetReps;
|
||||||
|
final double? targetScore;
|
||||||
|
final int? restSecondsOverride;
|
||||||
|
|
||||||
|
static ProgramExercise snapshotFromExercise({
|
||||||
|
required EntityMetadata metadata,
|
||||||
|
required String programId,
|
||||||
|
required Exercise exercise,
|
||||||
|
required int position,
|
||||||
|
required int setsCount,
|
||||||
|
required Set<WorkoutMeasure> enabledMeasures,
|
||||||
|
int? targetTimeSeconds,
|
||||||
|
int? targetReps,
|
||||||
|
double? targetScore,
|
||||||
|
int? restSecondsOverride,
|
||||||
|
}) {
|
||||||
|
final available = exercise.availableMeasures;
|
||||||
|
if (!available.containsAll(enabledMeasures)) {
|
||||||
|
throw const DomainException(
|
||||||
|
'Enabled measures must be a subset of exercise measures.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ProgramExercise(
|
||||||
|
metadata: metadata,
|
||||||
|
programId: programId,
|
||||||
|
sourceExerciseId: exercise.metadata.id,
|
||||||
|
position: position,
|
||||||
|
exerciseNameSnapshot: exercise.name,
|
||||||
|
exerciseDescriptionSnapshot: exercise.description,
|
||||||
|
exerciseImageMediaIdSnapshot: exercise.imageMediaId,
|
||||||
|
exerciseVideoMediaIdSnapshot: exercise.videoMediaId,
|
||||||
|
exerciseArchivedSnapshot: exercise.archivedAt != null,
|
||||||
|
availableTimeSnapshot: exercise.hasTimeMeasure,
|
||||||
|
availableRepsSnapshot: exercise.hasRepsMeasure,
|
||||||
|
availableScoreSnapshot: exercise.hasScoreMeasure,
|
||||||
|
scoreLabelSnapshot: exercise.scoreLabel,
|
||||||
|
scoreUnitSnapshot: exercise.scoreUnit,
|
||||||
|
setsCount: setsCount,
|
||||||
|
timeEnabled: enabledMeasures.contains(WorkoutMeasure.time),
|
||||||
|
repsEnabled: enabledMeasures.contains(WorkoutMeasure.reps),
|
||||||
|
scoreEnabled: enabledMeasures.contains(WorkoutMeasure.score),
|
||||||
|
targetTimeSeconds: targetTimeSeconds,
|
||||||
|
targetReps: targetReps,
|
||||||
|
targetScore: targetScore,
|
||||||
|
restSecondsOverride: restSecondsOverride,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object?> toSnapshotJson() => {
|
||||||
|
'id': metadata.id,
|
||||||
|
'sourceExerciseId': sourceExerciseId,
|
||||||
|
'position': position,
|
||||||
|
'exerciseNameSnapshot': exerciseNameSnapshot,
|
||||||
|
'exerciseDescriptionSnapshot': exerciseDescriptionSnapshot,
|
||||||
|
'exerciseImageMediaIdSnapshot': exerciseImageMediaIdSnapshot,
|
||||||
|
'exerciseVideoMediaIdSnapshot': exerciseVideoMediaIdSnapshot,
|
||||||
|
'exerciseArchivedSnapshot': exerciseArchivedSnapshot,
|
||||||
|
'availableTimeSnapshot': availableTimeSnapshot,
|
||||||
|
'availableRepsSnapshot': availableRepsSnapshot,
|
||||||
|
'availableScoreSnapshot': availableScoreSnapshot,
|
||||||
|
'scoreLabelSnapshot': scoreLabelSnapshot,
|
||||||
|
'scoreUnitSnapshot': scoreUnitSnapshot,
|
||||||
|
'setsCount': setsCount,
|
||||||
|
'timeEnabled': timeEnabled,
|
||||||
|
'repsEnabled': repsEnabled,
|
||||||
|
'scoreEnabled': scoreEnabled,
|
||||||
|
'targetTimeSeconds': targetTimeSeconds,
|
||||||
|
'targetReps': targetReps,
|
||||||
|
'targetScore': targetScore,
|
||||||
|
'restSecondsOverride': restSecondsOverride,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WorkoutTemplate {
|
||||||
|
WorkoutTemplate({
|
||||||
|
required this.metadata,
|
||||||
|
required String name,
|
||||||
|
this.lastStartedAt,
|
||||||
|
this.programs = const [],
|
||||||
|
this.overrides = const [],
|
||||||
|
}) : name = _nonBlank(name, 'Workout template name');
|
||||||
|
|
||||||
|
final EntityMetadata metadata;
|
||||||
|
final String name;
|
||||||
|
final DateTime? lastStartedAt;
|
||||||
|
final List<WorkoutTemplateProgram> programs;
|
||||||
|
final List<WorkoutTemplateExerciseOverride> overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WorkoutTemplateProgram {
|
||||||
|
WorkoutTemplateProgram({
|
||||||
|
required this.metadata,
|
||||||
|
required this.workoutTemplateId,
|
||||||
|
this.sourceProgramId,
|
||||||
|
required this.position,
|
||||||
|
required String programNameSnapshot,
|
||||||
|
required this.defaultRestSecondsSnapshot,
|
||||||
|
required this.programSnapshotJson,
|
||||||
|
}) : programNameSnapshot = _nonBlank(
|
||||||
|
programNameSnapshot,
|
||||||
|
'Program snapshot name',
|
||||||
|
) {
|
||||||
|
_requireNonNegative(position, 'Position');
|
||||||
|
_requireNonNegative(defaultRestSecondsSnapshot, 'Default rest seconds');
|
||||||
|
}
|
||||||
|
|
||||||
|
final EntityMetadata metadata;
|
||||||
|
final String workoutTemplateId;
|
||||||
|
final String? sourceProgramId;
|
||||||
|
final int position;
|
||||||
|
final String programNameSnapshot;
|
||||||
|
final int defaultRestSecondsSnapshot;
|
||||||
|
final String programSnapshotJson;
|
||||||
|
|
||||||
|
static WorkoutTemplateProgram snapshotFromProgram({
|
||||||
|
required EntityMetadata metadata,
|
||||||
|
required String workoutTemplateId,
|
||||||
|
required Program program,
|
||||||
|
required int position,
|
||||||
|
}) {
|
||||||
|
return WorkoutTemplateProgram(
|
||||||
|
metadata: metadata,
|
||||||
|
workoutTemplateId: workoutTemplateId,
|
||||||
|
sourceProgramId: program.metadata.id,
|
||||||
|
position: position,
|
||||||
|
programNameSnapshot: program.name,
|
||||||
|
defaultRestSecondsSnapshot: program.defaultRestSeconds,
|
||||||
|
programSnapshotJson: jsonEncode({
|
||||||
|
'programId': program.metadata.id,
|
||||||
|
'name': program.name,
|
||||||
|
'defaultRestSeconds': program.defaultRestSeconds,
|
||||||
|
'exercises': program.exercises
|
||||||
|
.map((exercise) => exercise.toSnapshotJson())
|
||||||
|
.toList(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WorkoutTemplateExerciseOverride {
|
||||||
|
WorkoutTemplateExerciseOverride({
|
||||||
|
required this.metadata,
|
||||||
|
required this.workoutTemplateProgramId,
|
||||||
|
required String snapshotProgramExerciseId,
|
||||||
|
this.setsCountOverride,
|
||||||
|
this.targetTimeSecondsOverride,
|
||||||
|
this.targetRepsOverride,
|
||||||
|
this.targetScoreOverride,
|
||||||
|
}) : snapshotProgramExerciseId = _nonBlank(
|
||||||
|
snapshotProgramExerciseId,
|
||||||
|
'Snapshot program exercise id',
|
||||||
|
) {
|
||||||
|
_requireNullablePositive(setsCountOverride, 'Sets count override');
|
||||||
|
_requireNullablePositive(
|
||||||
|
targetTimeSecondsOverride,
|
||||||
|
'Target time seconds override',
|
||||||
|
);
|
||||||
|
_requireNullablePositive(targetRepsOverride, 'Target reps override');
|
||||||
|
_requireNullableNonNegativeDouble(
|
||||||
|
targetScoreOverride,
|
||||||
|
'Target score override',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final EntityMetadata metadata;
|
||||||
|
final String workoutTemplateProgramId;
|
||||||
|
final String snapshotProgramExerciseId;
|
||||||
|
final int? setsCountOverride;
|
||||||
|
final int? targetTimeSecondsOverride;
|
||||||
|
final int? targetRepsOverride;
|
||||||
|
final double? targetScoreOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ActiveWorkoutSession {
|
||||||
|
ActiveWorkoutSession({
|
||||||
|
required this.metadata,
|
||||||
|
this.sourceWorkoutTemplateId,
|
||||||
|
required this.status,
|
||||||
|
required this.startedAt,
|
||||||
|
this.pausedAt,
|
||||||
|
this.endedAt,
|
||||||
|
required this.lastPersistedAt,
|
||||||
|
required this.elapsedActiveMs,
|
||||||
|
required this.currentProgramIndex,
|
||||||
|
required this.currentExerciseIndex,
|
||||||
|
required this.currentSetIndex,
|
||||||
|
required this.resolvedTemplateSnapshotJson,
|
||||||
|
}) {
|
||||||
|
_requireNonNegative(elapsedActiveMs, 'Elapsed active milliseconds');
|
||||||
|
_requireNonNegative(currentProgramIndex, 'Current program index');
|
||||||
|
_requireNonNegative(currentExerciseIndex, 'Current exercise index');
|
||||||
|
_requireNonNegative(currentSetIndex, 'Current set index');
|
||||||
|
}
|
||||||
|
|
||||||
|
final EntityMetadata metadata;
|
||||||
|
final String? sourceWorkoutTemplateId;
|
||||||
|
final ActiveWorkoutStatus status;
|
||||||
|
final DateTime startedAt;
|
||||||
|
final DateTime? pausedAt;
|
||||||
|
final DateTime? endedAt;
|
||||||
|
final DateTime lastPersistedAt;
|
||||||
|
final int elapsedActiveMs;
|
||||||
|
final int currentProgramIndex;
|
||||||
|
final int currentExerciseIndex;
|
||||||
|
final int currentSetIndex;
|
||||||
|
final String resolvedTemplateSnapshotJson;
|
||||||
|
|
||||||
|
int elapsedActiveMillisecondsAt(DateTime now) {
|
||||||
|
if (status != ActiveWorkoutStatus.running) {
|
||||||
|
return elapsedActiveMs;
|
||||||
|
}
|
||||||
|
return elapsedActiveMs + now.difference(lastPersistedAt).inMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveWorkoutSession pause(DateTime now) {
|
||||||
|
return copyWith(
|
||||||
|
status: ActiveWorkoutStatus.paused,
|
||||||
|
pausedAt: now,
|
||||||
|
lastPersistedAt: now,
|
||||||
|
elapsedActiveMs: elapsedActiveMillisecondsAt(now),
|
||||||
|
metadata: metadata.touch(now),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveWorkoutSession resume(DateTime now) {
|
||||||
|
return ActiveWorkoutSession(
|
||||||
|
metadata: metadata.touch(now),
|
||||||
|
sourceWorkoutTemplateId: sourceWorkoutTemplateId,
|
||||||
|
status: ActiveWorkoutStatus.running,
|
||||||
|
startedAt: startedAt,
|
||||||
|
pausedAt: null,
|
||||||
|
endedAt: endedAt,
|
||||||
|
lastPersistedAt: now,
|
||||||
|
elapsedActiveMs: elapsedActiveMs,
|
||||||
|
currentProgramIndex: currentProgramIndex,
|
||||||
|
currentExerciseIndex: currentExerciseIndex,
|
||||||
|
currentSetIndex: currentSetIndex,
|
||||||
|
resolvedTemplateSnapshotJson: resolvedTemplateSnapshotJson,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveWorkoutSession copyWith({
|
||||||
|
EntityMetadata? metadata,
|
||||||
|
ActiveWorkoutStatus? status,
|
||||||
|
DateTime? pausedAt,
|
||||||
|
DateTime? endedAt,
|
||||||
|
DateTime? lastPersistedAt,
|
||||||
|
int? elapsedActiveMs,
|
||||||
|
int? currentProgramIndex,
|
||||||
|
int? currentExerciseIndex,
|
||||||
|
int? currentSetIndex,
|
||||||
|
}) {
|
||||||
|
return ActiveWorkoutSession(
|
||||||
|
metadata: metadata ?? this.metadata,
|
||||||
|
sourceWorkoutTemplateId: sourceWorkoutTemplateId,
|
||||||
|
status: status ?? this.status,
|
||||||
|
startedAt: startedAt,
|
||||||
|
pausedAt: pausedAt,
|
||||||
|
endedAt: endedAt ?? this.endedAt,
|
||||||
|
lastPersistedAt: lastPersistedAt ?? this.lastPersistedAt,
|
||||||
|
elapsedActiveMs: elapsedActiveMs ?? this.elapsedActiveMs,
|
||||||
|
currentProgramIndex: currentProgramIndex ?? this.currentProgramIndex,
|
||||||
|
currentExerciseIndex: currentExerciseIndex ?? this.currentExerciseIndex,
|
||||||
|
currentSetIndex: currentSetIndex ?? this.currentSetIndex,
|
||||||
|
resolvedTemplateSnapshotJson: resolvedTemplateSnapshotJson,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ActiveSetResult {
|
||||||
|
const ActiveSetResult({
|
||||||
|
required this.metadata,
|
||||||
|
required this.activeWorkoutSessionId,
|
||||||
|
required this.programSnapshotId,
|
||||||
|
required this.exerciseSnapshotId,
|
||||||
|
required this.programIndex,
|
||||||
|
required this.exerciseIndex,
|
||||||
|
required this.setIndex,
|
||||||
|
this.startedAt,
|
||||||
|
this.completedAt,
|
||||||
|
this.actualTimeMs,
|
||||||
|
this.actualReps,
|
||||||
|
this.actualScore,
|
||||||
|
this.scoreLabelSnapshot,
|
||||||
|
this.scoreUnitSnapshot,
|
||||||
|
this.note,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EntityMetadata metadata;
|
||||||
|
final String activeWorkoutSessionId;
|
||||||
|
final String programSnapshotId;
|
||||||
|
final String exerciseSnapshotId;
|
||||||
|
final int programIndex;
|
||||||
|
final int exerciseIndex;
|
||||||
|
final int setIndex;
|
||||||
|
final DateTime? startedAt;
|
||||||
|
final DateTime? completedAt;
|
||||||
|
final int? actualTimeMs;
|
||||||
|
final int? actualReps;
|
||||||
|
final double? actualScore;
|
||||||
|
final String? scoreLabelSnapshot;
|
||||||
|
final String? scoreUnitSnapshot;
|
||||||
|
final String? note;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ActiveRestState {
|
||||||
|
const ActiveRestState({
|
||||||
|
required this.metadata,
|
||||||
|
required this.activeWorkoutSessionId,
|
||||||
|
required this.afterProgramIndex,
|
||||||
|
required this.afterExerciseIndex,
|
||||||
|
required this.afterSetIndex,
|
||||||
|
required this.plannedRestSeconds,
|
||||||
|
required this.adjustedRestSeconds,
|
||||||
|
required this.startedAt,
|
||||||
|
this.endedAt,
|
||||||
|
this.skippedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EntityMetadata metadata;
|
||||||
|
final String activeWorkoutSessionId;
|
||||||
|
final int afterProgramIndex;
|
||||||
|
final int afterExerciseIndex;
|
||||||
|
final int afterSetIndex;
|
||||||
|
final int plannedRestSeconds;
|
||||||
|
final int adjustedRestSeconds;
|
||||||
|
final DateTime startedAt;
|
||||||
|
final DateTime? endedAt;
|
||||||
|
final DateTime? skippedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WorkoutHistory {
|
||||||
|
const WorkoutHistory({
|
||||||
|
required this.metadata,
|
||||||
|
this.sourceWorkoutTemplateId,
|
||||||
|
this.sourceActiveWorkoutSessionId,
|
||||||
|
required this.nameSnapshot,
|
||||||
|
required this.startedAt,
|
||||||
|
required this.endedAt,
|
||||||
|
required this.totalActiveMs,
|
||||||
|
required this.completed,
|
||||||
|
required this.historySnapshotJson,
|
||||||
|
this.results = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
final EntityMetadata metadata;
|
||||||
|
final String? sourceWorkoutTemplateId;
|
||||||
|
final String? sourceActiveWorkoutSessionId;
|
||||||
|
final String nameSnapshot;
|
||||||
|
final DateTime startedAt;
|
||||||
|
final DateTime endedAt;
|
||||||
|
final int totalActiveMs;
|
||||||
|
final bool completed;
|
||||||
|
final String historySnapshotJson;
|
||||||
|
final List<WorkoutHistorySetResult> results;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class WorkoutHistorySetResult {
|
||||||
|
const WorkoutHistorySetResult({
|
||||||
|
required this.metadata,
|
||||||
|
required this.workoutHistoryId,
|
||||||
|
required this.programSnapshotId,
|
||||||
|
required this.exerciseSnapshotId,
|
||||||
|
required this.programIndex,
|
||||||
|
required this.exerciseIndex,
|
||||||
|
required this.setIndex,
|
||||||
|
required this.programNameSnapshot,
|
||||||
|
required this.exerciseNameSnapshot,
|
||||||
|
required this.timeEnabledSnapshot,
|
||||||
|
required this.repsEnabledSnapshot,
|
||||||
|
required this.scoreEnabledSnapshot,
|
||||||
|
this.targetTimeSecondsSnapshot,
|
||||||
|
this.targetRepsSnapshot,
|
||||||
|
this.targetScoreSnapshot,
|
||||||
|
this.actualTimeMs,
|
||||||
|
this.actualReps,
|
||||||
|
this.actualScore,
|
||||||
|
this.scoreLabelSnapshot,
|
||||||
|
this.scoreUnitSnapshot,
|
||||||
|
this.startedAt,
|
||||||
|
this.completedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EntityMetadata metadata;
|
||||||
|
final String workoutHistoryId;
|
||||||
|
final String programSnapshotId;
|
||||||
|
final String exerciseSnapshotId;
|
||||||
|
final int programIndex;
|
||||||
|
final int exerciseIndex;
|
||||||
|
final int setIndex;
|
||||||
|
final String programNameSnapshot;
|
||||||
|
final String exerciseNameSnapshot;
|
||||||
|
final bool timeEnabledSnapshot;
|
||||||
|
final bool repsEnabledSnapshot;
|
||||||
|
final bool scoreEnabledSnapshot;
|
||||||
|
final int? targetTimeSecondsSnapshot;
|
||||||
|
final int? targetRepsSnapshot;
|
||||||
|
final double? targetScoreSnapshot;
|
||||||
|
final int? actualTimeMs;
|
||||||
|
final int? actualReps;
|
||||||
|
final double? actualScore;
|
||||||
|
final String? scoreLabelSnapshot;
|
||||||
|
final String? scoreUnitSnapshot;
|
||||||
|
final DateTime? startedAt;
|
||||||
|
final DateTime? completedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _nonBlank(String? value, String label) {
|
||||||
|
final trimmed = value?.trim();
|
||||||
|
if (trimmed == null || trimmed.isEmpty) {
|
||||||
|
throw DomainException('$label must not be blank.');
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _requireAtLeastOneMeasure({
|
||||||
|
required bool hasTime,
|
||||||
|
required bool hasReps,
|
||||||
|
required bool hasScore,
|
||||||
|
}) {
|
||||||
|
if (!hasTime && !hasReps && !hasScore) {
|
||||||
|
throw const DomainException('At least one measure must be active.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _requirePositive(int value, String label) {
|
||||||
|
if (value <= 0) {
|
||||||
|
throw DomainException('$label must be positive.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _requireNonNegative(int value, String label) {
|
||||||
|
if (value < 0) {
|
||||||
|
throw DomainException('$label must not be negative.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _requireNullablePositive(int? value, String label) {
|
||||||
|
if (value != null) {
|
||||||
|
_requirePositive(value, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _requireNullableNonNegative(int? value, String label) {
|
||||||
|
if (value != null) {
|
||||||
|
_requireNonNegative(value, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _requireNullableNonNegativeDouble(double? value, String label) {
|
||||||
|
if (value != null && value < 0) {
|
||||||
|
throw DomainException('$label must not be negative.');
|
||||||
|
}
|
||||||
|
}
|
||||||
751
lib/infrastructure/local/drift_repositories.dart
Normal file
751
lib/infrastructure/local/drift_repositories.dart
Normal file
@ -0,0 +1,751 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
|
||||||
|
import '../../application/application.dart';
|
||||||
|
import '../../domain/domain.dart' as domain;
|
||||||
|
import 'app_database.dart' as db;
|
||||||
|
|
||||||
|
final class DriftExerciseRepository implements ExerciseRepository {
|
||||||
|
const DriftExerciseRepository(this.database);
|
||||||
|
|
||||||
|
final db.AppDatabase database;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<domain.Exercise?> findById(String id) async {
|
||||||
|
final row = await (database.select(
|
||||||
|
database.exercises,
|
||||||
|
)..where((table) => table.id.equals(id))).getSingleOrNull();
|
||||||
|
return row == null ? null : _exerciseFromRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<domain.Exercise>> listActive() async {
|
||||||
|
final rows =
|
||||||
|
await (database.select(database.exercises)
|
||||||
|
..where((table) => table.deletedAt.isNull())
|
||||||
|
..orderBy([(table) => OrderingTerm.asc(table.name)]))
|
||||||
|
.get();
|
||||||
|
return rows.map(_exerciseFromRow).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(domain.Exercise exercise) async {
|
||||||
|
await database
|
||||||
|
.into(database.exercises)
|
||||||
|
.insertOnConflictUpdate(_exerciseCompanion(exercise));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class DriftProgramRepository implements ProgramRepository {
|
||||||
|
const DriftProgramRepository(this.database);
|
||||||
|
|
||||||
|
final db.AppDatabase database;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<domain.Program?> findById(String id) async {
|
||||||
|
final row = await (database.select(
|
||||||
|
database.programs,
|
||||||
|
)..where((table) => table.id.equals(id))).getSingleOrNull();
|
||||||
|
if (row == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final exercises =
|
||||||
|
await (database.select(database.programExercises)
|
||||||
|
..where((table) => table.programId.equals(id))
|
||||||
|
..orderBy([(table) => OrderingTerm.asc(table.position)]))
|
||||||
|
.get();
|
||||||
|
return _programFromRow(
|
||||||
|
row,
|
||||||
|
exercises.map(_programExerciseFromRow).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(domain.Program program) async {
|
||||||
|
await database
|
||||||
|
.into(database.programs)
|
||||||
|
.insertOnConflictUpdate(_programCompanion(program));
|
||||||
|
for (final exercise in program.exercises) {
|
||||||
|
await saveExercise(exercise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveExercise(domain.ProgramExercise exercise) async {
|
||||||
|
await database
|
||||||
|
.into(database.programExercises)
|
||||||
|
.insertOnConflictUpdate(_programExerciseCompanion(exercise));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class DriftWorkoutTemplateRepository
|
||||||
|
implements WorkoutTemplateRepository {
|
||||||
|
const DriftWorkoutTemplateRepository(this.database);
|
||||||
|
|
||||||
|
final db.AppDatabase database;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<domain.WorkoutTemplate?> findById(String id) async {
|
||||||
|
final row = await (database.select(
|
||||||
|
database.workoutTemplates,
|
||||||
|
)..where((table) => table.id.equals(id))).getSingleOrNull();
|
||||||
|
if (row == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final programs =
|
||||||
|
await (database.select(database.workoutTemplatePrograms)
|
||||||
|
..where((table) => table.workoutTemplateId.equals(id))
|
||||||
|
..orderBy([(table) => OrderingTerm.asc(table.position)]))
|
||||||
|
.get();
|
||||||
|
final programIds = programs.map((program) => program.id).toSet();
|
||||||
|
final overrides = await (database.select(
|
||||||
|
database.workoutTemplateExerciseOverrides,
|
||||||
|
)..where((table) => table.workoutTemplateProgramId.isIn(programIds))).get();
|
||||||
|
return _workoutTemplateFromRow(
|
||||||
|
row,
|
||||||
|
programs.map(_workoutTemplateProgramFromRow).toList(),
|
||||||
|
overrides.map(_workoutTemplateExerciseOverrideFromRow).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(domain.WorkoutTemplate template) async {
|
||||||
|
await database
|
||||||
|
.into(database.workoutTemplates)
|
||||||
|
.insertOnConflictUpdate(_workoutTemplateCompanion(template));
|
||||||
|
for (final program in template.programs) {
|
||||||
|
await saveProgram(program);
|
||||||
|
}
|
||||||
|
for (final override in template.overrides) {
|
||||||
|
await saveOverride(override);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveProgram(domain.WorkoutTemplateProgram program) async {
|
||||||
|
await database
|
||||||
|
.into(database.workoutTemplatePrograms)
|
||||||
|
.insertOnConflictUpdate(_workoutTemplateProgramCompanion(program));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveOverride(
|
||||||
|
domain.WorkoutTemplateExerciseOverride override,
|
||||||
|
) async {
|
||||||
|
await database
|
||||||
|
.into(database.workoutTemplateExerciseOverrides)
|
||||||
|
.insertOnConflictUpdate(
|
||||||
|
_workoutTemplateExerciseOverrideCompanion(override),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
||||||
|
const DriftActiveSessionRepository(this.database);
|
||||||
|
|
||||||
|
final db.AppDatabase database;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<domain.ActiveWorkoutSession?> findById(String id) async {
|
||||||
|
final row = await (database.select(
|
||||||
|
database.activeWorkoutSessions,
|
||||||
|
)..where((table) => table.id.equals(id))).getSingleOrNull();
|
||||||
|
return row == null ? null : _activeWorkoutSessionFromRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(domain.ActiveWorkoutSession session) async {
|
||||||
|
await database
|
||||||
|
.into(database.activeWorkoutSessions)
|
||||||
|
.insertOnConflictUpdate(_activeWorkoutSessionCompanion(session));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveSetResult(domain.ActiveSetResult result) async {
|
||||||
|
await database
|
||||||
|
.into(database.activeSetResults)
|
||||||
|
.insertOnConflictUpdate(_activeSetResultCompanion(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveRestState(domain.ActiveRestState restState) async {
|
||||||
|
await database
|
||||||
|
.into(database.activeRestStates)
|
||||||
|
.insertOnConflictUpdate(_activeRestStateCompanion(restState));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<domain.ActiveSetResult>> listSetResults(String sessionId) async {
|
||||||
|
final rows =
|
||||||
|
await (database.select(database.activeSetResults)
|
||||||
|
..where((table) => table.activeWorkoutSessionId.equals(sessionId))
|
||||||
|
..orderBy([
|
||||||
|
(table) => OrderingTerm.asc(table.programIndex),
|
||||||
|
(table) => OrderingTerm.asc(table.exerciseIndex),
|
||||||
|
(table) => OrderingTerm.asc(table.setIndex),
|
||||||
|
]))
|
||||||
|
.get();
|
||||||
|
return rows.map(_activeSetResultFromRow).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<domain.ActiveRestState>> listRestStates(String sessionId) async {
|
||||||
|
final rows =
|
||||||
|
await (database.select(database.activeRestStates)
|
||||||
|
..where((table) => table.activeWorkoutSessionId.equals(sessionId))
|
||||||
|
..orderBy([(table) => OrderingTerm.asc(table.startedAt)]))
|
||||||
|
.get();
|
||||||
|
return rows.map(_activeRestStateFromRow).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class DriftWorkoutHistoryRepository implements WorkoutHistoryRepository {
|
||||||
|
const DriftWorkoutHistoryRepository(this.database);
|
||||||
|
|
||||||
|
final db.AppDatabase database;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(domain.WorkoutHistory history) async {
|
||||||
|
await database
|
||||||
|
.into(database.workoutHistories)
|
||||||
|
.insertOnConflictUpdate(_workoutHistoryCompanion(history));
|
||||||
|
for (final result in history.results) {
|
||||||
|
await saveSetResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveSetResult(domain.WorkoutHistorySetResult result) async {
|
||||||
|
await database
|
||||||
|
.into(database.workoutHistorySetResults)
|
||||||
|
.insertOnConflictUpdate(_workoutHistorySetResultCompanion(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.EntityMetadata _metadataFromRow(dynamic row) {
|
||||||
|
return domain.EntityMetadata(
|
||||||
|
id: row.id as String,
|
||||||
|
createdAt: row.createdAt as DateTime,
|
||||||
|
updatedAt: row.updatedAt as DateTime,
|
||||||
|
deletedAt: row.deletedAt as DateTime?,
|
||||||
|
schemaVersion: row.schemaVersion as int,
|
||||||
|
syncState: _syncStateFromDb(row.syncState as String),
|
||||||
|
localRevision: row.localRevision as int,
|
||||||
|
originDeviceId: row.originDeviceId as String,
|
||||||
|
futureOwnerProfileId: row.futureOwnerProfileId as String?,
|
||||||
|
lastSyncedAt: row.lastSyncedAt as DateTime?,
|
||||||
|
remoteRevision: row.remoteRevision as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Value<Object?>> _metadataValues(domain.EntityMetadata metadata) => [
|
||||||
|
Value(metadata.id),
|
||||||
|
Value(metadata.createdAt),
|
||||||
|
Value(metadata.updatedAt),
|
||||||
|
Value(metadata.deletedAt),
|
||||||
|
Value(metadata.schemaVersion),
|
||||||
|
Value(_syncStateToDb(metadata.syncState)),
|
||||||
|
Value(metadata.localRevision),
|
||||||
|
Value(metadata.originDeviceId),
|
||||||
|
Value(metadata.futureOwnerProfileId),
|
||||||
|
Value(metadata.lastSyncedAt),
|
||||||
|
Value(metadata.remoteRevision),
|
||||||
|
];
|
||||||
|
|
||||||
|
db.ExercisesCompanion _exerciseCompanion(domain.Exercise exercise) {
|
||||||
|
final values = _metadataValues(exercise.metadata);
|
||||||
|
return db.ExercisesCompanion(
|
||||||
|
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?>,
|
||||||
|
name: Value(exercise.name),
|
||||||
|
description: Value(exercise.description),
|
||||||
|
imageMediaId: Value(exercise.imageMediaId),
|
||||||
|
videoMediaId: Value(exercise.videoMediaId),
|
||||||
|
hasTimeMeasure: Value(exercise.hasTimeMeasure),
|
||||||
|
hasRepsMeasure: Value(exercise.hasRepsMeasure),
|
||||||
|
hasScoreMeasure: Value(exercise.hasScoreMeasure),
|
||||||
|
scoreLabel: Value(exercise.scoreLabel),
|
||||||
|
scoreUnit: Value(exercise.scoreUnit),
|
||||||
|
archivedAt: Value(exercise.archivedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.Exercise _exerciseFromRow(db.Exercise row) {
|
||||||
|
return domain.Exercise(
|
||||||
|
metadata: _metadataFromRow(row),
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
imageMediaId: row.imageMediaId,
|
||||||
|
videoMediaId: row.videoMediaId,
|
||||||
|
hasTimeMeasure: row.hasTimeMeasure,
|
||||||
|
hasRepsMeasure: row.hasRepsMeasure,
|
||||||
|
hasScoreMeasure: row.hasScoreMeasure,
|
||||||
|
scoreLabel: row.scoreLabel,
|
||||||
|
scoreUnit: row.scoreUnit,
|
||||||
|
archivedAt: row.archivedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.ProgramsCompanion _programCompanion(domain.Program program) {
|
||||||
|
final values = _metadataValues(program.metadata);
|
||||||
|
return db.ProgramsCompanion(
|
||||||
|
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?>,
|
||||||
|
name: Value(program.name),
|
||||||
|
defaultRestSeconds: Value(program.defaultRestSeconds),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.Program _programFromRow(
|
||||||
|
db.Program row,
|
||||||
|
List<domain.ProgramExercise> exercises,
|
||||||
|
) {
|
||||||
|
return domain.Program(
|
||||||
|
metadata: _metadataFromRow(row),
|
||||||
|
name: row.name,
|
||||||
|
defaultRestSeconds: row.defaultRestSeconds,
|
||||||
|
exercises: exercises,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.ProgramExercisesCompanion _programExerciseCompanion(
|
||||||
|
domain.ProgramExercise exercise,
|
||||||
|
) {
|
||||||
|
final values = _metadataValues(exercise.metadata);
|
||||||
|
return db.ProgramExercisesCompanion(
|
||||||
|
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?>,
|
||||||
|
programId: Value(exercise.programId),
|
||||||
|
sourceExerciseId: Value(exercise.sourceExerciseId),
|
||||||
|
position: Value(exercise.position),
|
||||||
|
exerciseNameSnapshot: Value(exercise.exerciseNameSnapshot),
|
||||||
|
exerciseDescriptionSnapshot: Value(exercise.exerciseDescriptionSnapshot),
|
||||||
|
exerciseImageMediaIdSnapshot: Value(exercise.exerciseImageMediaIdSnapshot),
|
||||||
|
exerciseVideoMediaIdSnapshot: Value(exercise.exerciseVideoMediaIdSnapshot),
|
||||||
|
exerciseArchivedSnapshot: Value(exercise.exerciseArchivedSnapshot),
|
||||||
|
availableTimeSnapshot: Value(exercise.availableTimeSnapshot),
|
||||||
|
availableRepsSnapshot: Value(exercise.availableRepsSnapshot),
|
||||||
|
availableScoreSnapshot: Value(exercise.availableScoreSnapshot),
|
||||||
|
scoreLabelSnapshot: Value(exercise.scoreLabelSnapshot),
|
||||||
|
scoreUnitSnapshot: Value(exercise.scoreUnitSnapshot),
|
||||||
|
setsCount: Value(exercise.setsCount),
|
||||||
|
timeEnabled: Value(exercise.timeEnabled),
|
||||||
|
repsEnabled: Value(exercise.repsEnabled),
|
||||||
|
scoreEnabled: Value(exercise.scoreEnabled),
|
||||||
|
targetTimeSeconds: Value(exercise.targetTimeSeconds),
|
||||||
|
targetReps: Value(exercise.targetReps),
|
||||||
|
targetScore: Value(exercise.targetScore),
|
||||||
|
restSecondsOverride: Value(exercise.restSecondsOverride),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.ProgramExercise _programExerciseFromRow(db.ProgramExercise row) {
|
||||||
|
return domain.ProgramExercise(
|
||||||
|
metadata: _metadataFromRow(row),
|
||||||
|
programId: row.programId,
|
||||||
|
sourceExerciseId: row.sourceExerciseId,
|
||||||
|
position: row.position,
|
||||||
|
exerciseNameSnapshot: row.exerciseNameSnapshot,
|
||||||
|
exerciseDescriptionSnapshot: row.exerciseDescriptionSnapshot,
|
||||||
|
exerciseImageMediaIdSnapshot: row.exerciseImageMediaIdSnapshot,
|
||||||
|
exerciseVideoMediaIdSnapshot: row.exerciseVideoMediaIdSnapshot,
|
||||||
|
exerciseArchivedSnapshot: row.exerciseArchivedSnapshot,
|
||||||
|
availableTimeSnapshot: row.availableTimeSnapshot,
|
||||||
|
availableRepsSnapshot: row.availableRepsSnapshot,
|
||||||
|
availableScoreSnapshot: row.availableScoreSnapshot,
|
||||||
|
scoreLabelSnapshot: row.scoreLabelSnapshot,
|
||||||
|
scoreUnitSnapshot: row.scoreUnitSnapshot,
|
||||||
|
setsCount: row.setsCount,
|
||||||
|
timeEnabled: row.timeEnabled,
|
||||||
|
repsEnabled: row.repsEnabled,
|
||||||
|
scoreEnabled: row.scoreEnabled,
|
||||||
|
targetTimeSeconds: row.targetTimeSeconds,
|
||||||
|
targetReps: row.targetReps,
|
||||||
|
targetScore: row.targetScore,
|
||||||
|
restSecondsOverride: row.restSecondsOverride,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.WorkoutTemplatesCompanion _workoutTemplateCompanion(
|
||||||
|
domain.WorkoutTemplate template,
|
||||||
|
) {
|
||||||
|
final values = _metadataValues(template.metadata);
|
||||||
|
return db.WorkoutTemplatesCompanion(
|
||||||
|
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?>,
|
||||||
|
name: Value(template.name),
|
||||||
|
lastStartedAt: Value(template.lastStartedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.WorkoutTemplate _workoutTemplateFromRow(
|
||||||
|
db.WorkoutTemplate row,
|
||||||
|
List<domain.WorkoutTemplateProgram> programs,
|
||||||
|
List<domain.WorkoutTemplateExerciseOverride> overrides,
|
||||||
|
) {
|
||||||
|
return domain.WorkoutTemplate(
|
||||||
|
metadata: _metadataFromRow(row),
|
||||||
|
name: row.name,
|
||||||
|
lastStartedAt: row.lastStartedAt,
|
||||||
|
programs: programs,
|
||||||
|
overrides: overrides,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.WorkoutTemplateProgramsCompanion _workoutTemplateProgramCompanion(
|
||||||
|
domain.WorkoutTemplateProgram program,
|
||||||
|
) {
|
||||||
|
final values = _metadataValues(program.metadata);
|
||||||
|
return db.WorkoutTemplateProgramsCompanion(
|
||||||
|
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?>,
|
||||||
|
workoutTemplateId: Value(program.workoutTemplateId),
|
||||||
|
sourceProgramId: Value(program.sourceProgramId),
|
||||||
|
position: Value(program.position),
|
||||||
|
programNameSnapshot: Value(program.programNameSnapshot),
|
||||||
|
defaultRestSecondsSnapshot: Value(program.defaultRestSecondsSnapshot),
|
||||||
|
programSnapshotJson: Value(program.programSnapshotJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.WorkoutTemplateProgram _workoutTemplateProgramFromRow(
|
||||||
|
db.WorkoutTemplateProgram row,
|
||||||
|
) {
|
||||||
|
return domain.WorkoutTemplateProgram(
|
||||||
|
metadata: _metadataFromRow(row),
|
||||||
|
workoutTemplateId: row.workoutTemplateId,
|
||||||
|
sourceProgramId: row.sourceProgramId,
|
||||||
|
position: row.position,
|
||||||
|
programNameSnapshot: row.programNameSnapshot,
|
||||||
|
defaultRestSecondsSnapshot: row.defaultRestSecondsSnapshot,
|
||||||
|
programSnapshotJson: row.programSnapshotJson,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.WorkoutTemplateExerciseOverridesCompanion
|
||||||
|
_workoutTemplateExerciseOverrideCompanion(
|
||||||
|
domain.WorkoutTemplateExerciseOverride override,
|
||||||
|
) {
|
||||||
|
final values = _metadataValues(override.metadata);
|
||||||
|
return db.WorkoutTemplateExerciseOverridesCompanion(
|
||||||
|
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?>,
|
||||||
|
workoutTemplateProgramId: Value(override.workoutTemplateProgramId),
|
||||||
|
snapshotProgramExerciseId: Value(override.snapshotProgramExerciseId),
|
||||||
|
setsCountOverride: Value(override.setsCountOverride),
|
||||||
|
targetTimeSecondsOverride: Value(override.targetTimeSecondsOverride),
|
||||||
|
targetRepsOverride: Value(override.targetRepsOverride),
|
||||||
|
targetScoreOverride: Value(override.targetScoreOverride),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.WorkoutTemplateExerciseOverride _workoutTemplateExerciseOverrideFromRow(
|
||||||
|
db.WorkoutTemplateExerciseOverride row,
|
||||||
|
) {
|
||||||
|
return domain.WorkoutTemplateExerciseOverride(
|
||||||
|
metadata: _metadataFromRow(row),
|
||||||
|
workoutTemplateProgramId: row.workoutTemplateProgramId,
|
||||||
|
snapshotProgramExerciseId: row.snapshotProgramExerciseId,
|
||||||
|
setsCountOverride: row.setsCountOverride,
|
||||||
|
targetTimeSecondsOverride: row.targetTimeSecondsOverride,
|
||||||
|
targetRepsOverride: row.targetRepsOverride,
|
||||||
|
targetScoreOverride: row.targetScoreOverride,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.ActiveWorkoutSessionsCompanion _activeWorkoutSessionCompanion(
|
||||||
|
domain.ActiveWorkoutSession session,
|
||||||
|
) {
|
||||||
|
final values = _metadataValues(session.metadata);
|
||||||
|
return db.ActiveWorkoutSessionsCompanion(
|
||||||
|
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?>,
|
||||||
|
sourceWorkoutTemplateId: Value(session.sourceWorkoutTemplateId),
|
||||||
|
status: Value(_activeStatusToDb(session.status)),
|
||||||
|
startedAt: Value(session.startedAt),
|
||||||
|
pausedAt: Value(session.pausedAt),
|
||||||
|
endedAt: Value(session.endedAt),
|
||||||
|
lastPersistedAt: Value(session.lastPersistedAt),
|
||||||
|
elapsedActiveMs: Value(session.elapsedActiveMs),
|
||||||
|
currentProgramIndex: Value(session.currentProgramIndex),
|
||||||
|
currentExerciseIndex: Value(session.currentExerciseIndex),
|
||||||
|
currentSetIndex: Value(session.currentSetIndex),
|
||||||
|
resolvedTemplateSnapshotJson: Value(session.resolvedTemplateSnapshotJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.ActiveWorkoutSession _activeWorkoutSessionFromRow(
|
||||||
|
db.ActiveWorkoutSession row,
|
||||||
|
) {
|
||||||
|
return domain.ActiveWorkoutSession(
|
||||||
|
metadata: _metadataFromRow(row),
|
||||||
|
sourceWorkoutTemplateId: row.sourceWorkoutTemplateId,
|
||||||
|
status: _activeStatusFromDb(row.status),
|
||||||
|
startedAt: row.startedAt,
|
||||||
|
pausedAt: row.pausedAt,
|
||||||
|
endedAt: row.endedAt,
|
||||||
|
lastPersistedAt: row.lastPersistedAt,
|
||||||
|
elapsedActiveMs: row.elapsedActiveMs,
|
||||||
|
currentProgramIndex: row.currentProgramIndex,
|
||||||
|
currentExerciseIndex: row.currentExerciseIndex,
|
||||||
|
currentSetIndex: row.currentSetIndex,
|
||||||
|
resolvedTemplateSnapshotJson: row.resolvedTemplateSnapshotJson,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.ActiveSetResultsCompanion _activeSetResultCompanion(
|
||||||
|
domain.ActiveSetResult result,
|
||||||
|
) {
|
||||||
|
final values = _metadataValues(result.metadata);
|
||||||
|
return db.ActiveSetResultsCompanion(
|
||||||
|
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(result.activeWorkoutSessionId),
|
||||||
|
programSnapshotId: Value(result.programSnapshotId),
|
||||||
|
exerciseSnapshotId: Value(result.exerciseSnapshotId),
|
||||||
|
programIndex: Value(result.programIndex),
|
||||||
|
exerciseIndex: Value(result.exerciseIndex),
|
||||||
|
setIndex: Value(result.setIndex),
|
||||||
|
startedAt: Value(result.startedAt),
|
||||||
|
completedAt: Value(result.completedAt),
|
||||||
|
actualTimeMs: Value(result.actualTimeMs),
|
||||||
|
actualReps: Value(result.actualReps),
|
||||||
|
actualScore: Value(result.actualScore),
|
||||||
|
scoreLabelSnapshot: Value(result.scoreLabelSnapshot),
|
||||||
|
scoreUnitSnapshot: Value(result.scoreUnitSnapshot),
|
||||||
|
note: Value(result.note),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.ActiveSetResult _activeSetResultFromRow(db.ActiveSetResult row) {
|
||||||
|
return domain.ActiveSetResult(
|
||||||
|
metadata: _metadataFromRow(row),
|
||||||
|
activeWorkoutSessionId: row.activeWorkoutSessionId,
|
||||||
|
programSnapshotId: row.programSnapshotId,
|
||||||
|
exerciseSnapshotId: row.exerciseSnapshotId,
|
||||||
|
programIndex: row.programIndex,
|
||||||
|
exerciseIndex: row.exerciseIndex,
|
||||||
|
setIndex: row.setIndex,
|
||||||
|
startedAt: row.startedAt,
|
||||||
|
completedAt: row.completedAt,
|
||||||
|
actualTimeMs: row.actualTimeMs,
|
||||||
|
actualReps: row.actualReps,
|
||||||
|
actualScore: row.actualScore,
|
||||||
|
scoreLabelSnapshot: row.scoreLabelSnapshot,
|
||||||
|
scoreUnitSnapshot: row.scoreUnitSnapshot,
|
||||||
|
note: row.note,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.ActiveRestStatesCompanion _activeRestStateCompanion(
|
||||||
|
domain.ActiveRestState restState,
|
||||||
|
) {
|
||||||
|
final values = _metadataValues(restState.metadata);
|
||||||
|
return db.ActiveRestStatesCompanion(
|
||||||
|
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(restState.activeWorkoutSessionId),
|
||||||
|
afterProgramIndex: Value(restState.afterProgramIndex),
|
||||||
|
afterExerciseIndex: Value(restState.afterExerciseIndex),
|
||||||
|
afterSetIndex: Value(restState.afterSetIndex),
|
||||||
|
plannedRestSeconds: Value(restState.plannedRestSeconds),
|
||||||
|
adjustedRestSeconds: Value(restState.adjustedRestSeconds),
|
||||||
|
startedAt: Value(restState.startedAt),
|
||||||
|
endedAt: Value(restState.endedAt),
|
||||||
|
skippedAt: Value(restState.skippedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
domain.ActiveRestState _activeRestStateFromRow(db.ActiveRestState row) {
|
||||||
|
return domain.ActiveRestState(
|
||||||
|
metadata: _metadataFromRow(row),
|
||||||
|
activeWorkoutSessionId: row.activeWorkoutSessionId,
|
||||||
|
afterProgramIndex: row.afterProgramIndex,
|
||||||
|
afterExerciseIndex: row.afterExerciseIndex,
|
||||||
|
afterSetIndex: row.afterSetIndex,
|
||||||
|
plannedRestSeconds: row.plannedRestSeconds,
|
||||||
|
adjustedRestSeconds: row.adjustedRestSeconds,
|
||||||
|
startedAt: row.startedAt,
|
||||||
|
endedAt: row.endedAt,
|
||||||
|
skippedAt: row.skippedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.WorkoutHistoriesCompanion _workoutHistoryCompanion(
|
||||||
|
domain.WorkoutHistory history,
|
||||||
|
) {
|
||||||
|
final values = _metadataValues(history.metadata);
|
||||||
|
return db.WorkoutHistoriesCompanion(
|
||||||
|
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?>,
|
||||||
|
sourceWorkoutTemplateId: Value(history.sourceWorkoutTemplateId),
|
||||||
|
sourceActiveWorkoutSessionId: Value(history.sourceActiveWorkoutSessionId),
|
||||||
|
nameSnapshot: Value(history.nameSnapshot),
|
||||||
|
startedAt: Value(history.startedAt),
|
||||||
|
endedAt: Value(history.endedAt),
|
||||||
|
totalActiveMs: Value(history.totalActiveMs),
|
||||||
|
completed: Value(history.completed),
|
||||||
|
historySnapshotJson: Value(history.historySnapshotJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.WorkoutHistorySetResultsCompanion _workoutHistorySetResultCompanion(
|
||||||
|
domain.WorkoutHistorySetResult result,
|
||||||
|
) {
|
||||||
|
final values = _metadataValues(result.metadata);
|
||||||
|
return db.WorkoutHistorySetResultsCompanion(
|
||||||
|
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?>,
|
||||||
|
workoutHistoryId: Value(result.workoutHistoryId),
|
||||||
|
programSnapshotId: Value(result.programSnapshotId),
|
||||||
|
exerciseSnapshotId: Value(result.exerciseSnapshotId),
|
||||||
|
programIndex: Value(result.programIndex),
|
||||||
|
exerciseIndex: Value(result.exerciseIndex),
|
||||||
|
setIndex: Value(result.setIndex),
|
||||||
|
programNameSnapshot: Value(result.programNameSnapshot),
|
||||||
|
exerciseNameSnapshot: Value(result.exerciseNameSnapshot),
|
||||||
|
timeEnabledSnapshot: Value(result.timeEnabledSnapshot),
|
||||||
|
repsEnabledSnapshot: Value(result.repsEnabledSnapshot),
|
||||||
|
scoreEnabledSnapshot: Value(result.scoreEnabledSnapshot),
|
||||||
|
targetTimeSecondsSnapshot: Value(result.targetTimeSecondsSnapshot),
|
||||||
|
targetRepsSnapshot: Value(result.targetRepsSnapshot),
|
||||||
|
targetScoreSnapshot: Value(result.targetScoreSnapshot),
|
||||||
|
actualTimeMs: Value(result.actualTimeMs),
|
||||||
|
actualReps: Value(result.actualReps),
|
||||||
|
actualScore: Value(result.actualScore),
|
||||||
|
scoreLabelSnapshot: Value(result.scoreLabelSnapshot),
|
||||||
|
scoreUnitSnapshot: Value(result.scoreUnitSnapshot),
|
||||||
|
startedAt: Value(result.startedAt),
|
||||||
|
completedAt: Value(result.completedAt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _syncStateToDb(domain.SyncState state) => switch (state) {
|
||||||
|
domain.SyncState.localOnly => 'localOnly',
|
||||||
|
domain.SyncState.dirty => 'dirty',
|
||||||
|
domain.SyncState.synced => 'synced',
|
||||||
|
domain.SyncState.deleted => 'deleted',
|
||||||
|
};
|
||||||
|
|
||||||
|
domain.SyncState _syncStateFromDb(String value) => switch (value) {
|
||||||
|
'localOnly' => domain.SyncState.localOnly,
|
||||||
|
'dirty' => domain.SyncState.dirty,
|
||||||
|
'synced' => domain.SyncState.synced,
|
||||||
|
'deleted' => domain.SyncState.deleted,
|
||||||
|
_ => throw domain.DomainException('Unknown sync state: $value'),
|
||||||
|
};
|
||||||
|
|
||||||
|
String _activeStatusToDb(domain.ActiveWorkoutStatus status) => switch (status) {
|
||||||
|
domain.ActiveWorkoutStatus.running => 'running',
|
||||||
|
domain.ActiveWorkoutStatus.paused => 'paused',
|
||||||
|
domain.ActiveWorkoutStatus.savedExit => 'savedExit',
|
||||||
|
domain.ActiveWorkoutStatus.completed => 'completed',
|
||||||
|
domain.ActiveWorkoutStatus.abandoned => 'abandoned',
|
||||||
|
};
|
||||||
|
|
||||||
|
domain.ActiveWorkoutStatus _activeStatusFromDb(String value) => switch (value) {
|
||||||
|
'running' => domain.ActiveWorkoutStatus.running,
|
||||||
|
'paused' => domain.ActiveWorkoutStatus.paused,
|
||||||
|
'savedExit' => domain.ActiveWorkoutStatus.savedExit,
|
||||||
|
'completed' => domain.ActiveWorkoutStatus.completed,
|
||||||
|
'abandoned' => domain.ActiveWorkoutStatus.abandoned,
|
||||||
|
_ => throw domain.DomainException('Unknown active workout status: $value'),
|
||||||
|
};
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export 'app_database.dart';
|
export 'app_database.dart';
|
||||||
|
export 'drift_repositories.dart';
|
||||||
export 'local_id.dart';
|
export 'local_id.dart';
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import '../../application/ports.dart';
|
||||||
|
|
||||||
const _crockfordBase32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
const _crockfordBase32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
||||||
|
|
||||||
final class LocalIdGenerator {
|
final class LocalIdGenerator implements IdGenerator {
|
||||||
LocalIdGenerator({Random? random}) : _random = random ?? Random.secure();
|
LocalIdGenerator({Random? random}) : _random = random ?? Random.secure();
|
||||||
|
|
||||||
final Random _random;
|
final Random _random;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String newId() => newUlid();
|
||||||
|
|
||||||
String newUlid({DateTime? now}) {
|
String newUlid({DateTime? now}) {
|
||||||
final timestamp = (now ?? DateTime.now().toUtc()).millisecondsSinceEpoch;
|
final timestamp = (now ?? DateTime.now().toUtc()).millisecondsSinceEpoch;
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
|
|||||||
117
test/application/use_cases_test.dart
Normal file
117
test/application/use_cases_test.dart
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:gametime/application/application.dart';
|
||||||
|
import 'package:gametime/domain/domain.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('Exercise requires at least one active measure', () {
|
||||||
|
expect(
|
||||||
|
() => Exercise(
|
||||||
|
metadata: _metadata('exercise-1'),
|
||||||
|
name: 'Push up',
|
||||||
|
hasTimeMeasure: false,
|
||||||
|
hasRepsMeasure: false,
|
||||||
|
hasScoreMeasure: false,
|
||||||
|
),
|
||||||
|
throwsA(isA<DomainException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Workout template use case rejects active measure overrides', () async {
|
||||||
|
final useCase = WorkoutTemplateUseCases(
|
||||||
|
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||||
|
programRepository: _FakeProgramRepository(),
|
||||||
|
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||||
|
ids: _FakeIds(),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
useCase.overrideExerciseTargets(
|
||||||
|
workoutTemplateProgramId: 'template-program-1',
|
||||||
|
snapshotProgramExerciseId: 'exercise-snapshot-1',
|
||||||
|
attemptedMeasureOverride: {WorkoutMeasure.reps},
|
||||||
|
),
|
||||||
|
throwsA(isA<DomainException>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Active session elapsed time is based on persisted timestamps', () {
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final session = ActiveWorkoutSession(
|
||||||
|
metadata: _metadata('session-1'),
|
||||||
|
status: ActiveWorkoutStatus.running,
|
||||||
|
startedAt: clock.now(),
|
||||||
|
lastPersistedAt: clock.now(),
|
||||||
|
elapsedActiveMs: 0,
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
resolvedTemplateSnapshotJson: '{}',
|
||||||
|
);
|
||||||
|
|
||||||
|
clock.value = clock.value.add(const Duration(seconds: 30));
|
||||||
|
final paused = session.pause(clock.now());
|
||||||
|
expect(paused.elapsedActiveMs, 30000);
|
||||||
|
|
||||||
|
clock.value = clock.value.add(const Duration(minutes: 5));
|
||||||
|
final resumed = paused.resume(clock.now());
|
||||||
|
expect(resumed.elapsedActiveMillisecondsAt(clock.now()), 30000);
|
||||||
|
|
||||||
|
clock.value = clock.value.add(const Duration(seconds: 15));
|
||||||
|
expect(resumed.elapsedActiveMillisecondsAt(clock.now()), 45000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityMetadata _metadata(String id) {
|
||||||
|
return EntityMetadata(
|
||||||
|
id: id,
|
||||||
|
createdAt: DateTime.utc(2026, 7, 17),
|
||||||
|
updatedAt: DateTime.utc(2026, 7, 17),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _FakeClock implements Clock {
|
||||||
|
_FakeClock(this.value);
|
||||||
|
|
||||||
|
DateTime value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime now() => value;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _FakeIds implements IdGenerator {
|
||||||
|
var _next = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String newId() {
|
||||||
|
_next += 1;
|
||||||
|
return 'id-$_next';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _FakeWorkoutTemplateRepository
|
||||||
|
implements WorkoutTemplateRepository {
|
||||||
|
@override
|
||||||
|
Future<WorkoutTemplate?> findById(String id) async => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(WorkoutTemplate template) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveOverride(WorkoutTemplateExerciseOverride override) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveProgram(WorkoutTemplateProgram program) async {}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _FakeProgramRepository implements ProgramRepository {
|
||||||
|
@override
|
||||||
|
Future<Program?> findById(String id) async => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(Program program) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> saveExercise(ProgramExercise exercise) async {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user