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>
743 lines
22 KiB
Dart
743 lines
22 KiB
Dart
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.');
|
|
}
|
|
}
|