feat(application): couche application avec use cases et repositories
Ajoute les entités du domaine (domain/entities.dart), les ports de repository (application/ports.dart), les use cases (application/use_cases.dart) et leur implémentation Drift (infrastructure/local/drift_repositories.dart). flutter analyze propre, tests unitaires verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
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.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user