import 'dart:typed_data'; 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(); } final class StarterContent { const StarterContent({ required this.exercises, required this.program, required this.workoutTemplate, }); final List exercises; final Program program; final WorkoutTemplate workoutTemplate; } final class TagUsage { const TagUsage({required this.tag, required this.count}); final String tag; final int count; } enum LocalBackupImportMode { merge, replaceAll } enum LocalBackupValidationError { invalidFile, incompatibleFormat, newerVersion, corrupted, activeWorkoutInProgress, importFailedNoMutation, } final class LocalBackupException implements Exception { const LocalBackupException(this.error); final LocalBackupValidationError error; @override String toString() => error.name; } final class LocalBackupDocument { const LocalBackupDocument({required this.fileName, required this.bytes}); final String fileName; final Uint8List bytes; } final class LocalBackupCounts { const LocalBackupCounts({ required this.exercises, required this.programs, required this.workoutTemplates, required this.workoutHistories, required this.mediaAssets, required this.embeddedMediaFiles, }); final int exercises; final int programs; final int workoutTemplates; final int workoutHistories; final int mediaAssets; final int embeddedMediaFiles; } final class LocalBackupPreview { const LocalBackupPreview({ required this.exportedAt, required this.counts, required this.hasEmbeddedMedia, required this.missingMediaCount, required this.hasNameDuplicates, }); final DateTime exportedAt; final LocalBackupCounts counts; final bool hasEmbeddedMedia; final int missingMediaCount; final bool hasNameDuplicates; } final class LocalBackupImportResult { const LocalBackupImportResult({ required this.insertedCount, required this.updatedCount, required this.ignoredOlderCount, required this.deletedByReplaceCount, required this.missingMediaCount, }); final int insertedCount; final int updatedCount; final int ignoredOlderCount; final int deletedByReplaceCount; final int missingMediaCount; LocalBackupImportResult copyWith({int? missingMediaCount}) { return LocalBackupImportResult( insertedCount: insertedCount, updatedCount: updatedCount, ignoredOlderCount: ignoredOlderCount, deletedByReplaceCount: deletedByReplaceCount, missingMediaCount: missingMediaCount ?? this.missingMediaCount, ); } } final class LocalBackupResource { const LocalBackupResource({ required this.id, required this.updatedAt, required this.payload, }); final String id; final DateTime updatedAt; final Map payload; LocalBackupResource copyWith({Map? payload}) { return LocalBackupResource( id: id, updatedAt: updatedAt, payload: payload ?? this.payload, ); } } final class LocalDataExportSnapshot { const LocalDataExportSnapshot({ required this.exportedAt, required this.appSchemaVersion, required this.originDeviceId, required this.mediaAssets, required this.exercises, required this.programs, required this.workoutTemplates, required this.workoutHistories, this.mediaFiles = const [], }); final DateTime exportedAt; final int appSchemaVersion; final String originDeviceId; final List mediaAssets; final List exercises; final List programs; final List workoutTemplates; final List workoutHistories; final List mediaFiles; LocalBackupCounts get counts => LocalBackupCounts( exercises: exercises.length, programs: programs.length, workoutTemplates: workoutTemplates.length, workoutHistories: workoutHistories.length, mediaAssets: mediaAssets.length, embeddedMediaFiles: mediaFiles.length, ); LocalDataExportSnapshot copyWith({ List? mediaAssets, List? mediaFiles, }) { return LocalDataExportSnapshot( exportedAt: exportedAt, appSchemaVersion: appSchemaVersion, originDeviceId: originDeviceId, mediaAssets: mediaAssets ?? this.mediaAssets, exercises: exercises, programs: programs, workoutTemplates: workoutTemplates, workoutHistories: workoutHistories, mediaFiles: mediaFiles ?? this.mediaFiles, ); } } final class EmbeddedBackupMediaFile { const EmbeddedBackupMediaFile({ required this.mediaAssetId, required this.role, required this.fileName, required this.mimeType, required this.sizeBytes, required this.sha256, required this.base64, }); final String mediaAssetId; final String role; final String fileName; final String? mimeType; final int sizeBytes; final String sha256; final String base64; } final class RestoredMediaFile { const RestoredMediaFile({ required this.mediaAssetId, required this.localUri, required this.sizeBytes, }); final String mediaAssetId; final String localUri; final int sizeBytes; } abstract interface class LocalDataBackupRepository { Future readExportSnapshot(DateTime exportedAt); Future hasOpenActiveWorkoutSession(); Future hasAnyUserData(); Future applyImportSnapshot({ required LocalDataExportSnapshot snapshot, required LocalBackupImportMode mode, required DateTime importedAt, }); } abstract interface class LocalBackupMediaStore { Future> readEmbeddableFiles( List mediaAssets, ); Future> restoreEmbeddedFiles( List files, ); } enum ProgressionPeriod { fourWeeks, threeMonths, all } enum ProgressionMeasure { manualScore, stopwatchScore, reps, time } enum ProgressionRangeLabelKind { sinceDate, sinceFirstSession, none } enum ProgressionSeriesState { ready, singlePoint, emptyForPeriod, emptyButHasAllTimeData, noGraphableMeasure, textScoreOnly, } enum ProgressionSeriesSummaryKind { bestLast, totals, none } final class ProgressionDateRange { const ProgressionDateRange({required this.startedAt, required this.endedAt}); final DateTime? startedAt; final DateTime endedAt; } final class ProgressionGlobalStatsData { const ProgressionGlobalStatsData({ required this.completedSessionCount, required this.totalActiveMs, required this.activeWeekStarts, required this.hasAnyCompletedHistory, this.firstCompletedSessionAt, }); final int completedSessionCount; final int totalActiveMs; final List activeWeekStarts; final bool hasAnyCompletedHistory; final DateTime? firstCompletedSessionAt; } final class ProgressionExerciseOptionData { const ProgressionExerciseOptionData({ required this.exerciseKey, required this.nameSnapshot, required this.isArchived, required this.lastPerformedAt, }); final String exerciseKey; final String nameSnapshot; final bool isArchived; final DateTime lastPerformedAt; } final class ProgressionMeasureOptionData { const ProgressionMeasureOptionData({ required this.measure, required this.label, this.scoreLabel, this.scoreUnit, required this.lowerIsBetter, }); final ProgressionMeasure measure; final String label; final String? scoreLabel; final String? scoreUnit; final bool lowerIsBetter; } final class ProgressionExerciseSeriesData { const ProgressionExerciseSeriesData({ required this.exerciseKey, required this.exerciseNameSnapshot, required this.measure, required this.points, required this.hasAnyAllTimeData, required this.hasAnyCompletedExerciseResult, }); final String exerciseKey; final String exerciseNameSnapshot; final ProgressionMeasureOptionData measure; final List points; final bool hasAnyAllTimeData; final bool hasAnyCompletedExerciseResult; } final class ProgressionPointData { const ProgressionPointData({ required this.workoutHistoryId, required this.workoutNameSnapshot, required this.startedAt, required this.value, required this.rawValue, }); final String workoutHistoryId; final String workoutNameSnapshot; final DateTime startedAt; final double value; final Object rawValue; } abstract interface class ProgressionStatsRepository { Future readGlobalStats( ProgressionDateRange range, ); Future> listExerciseOptions( ProgressionDateRange range, ); Future> listMeasureOptions({ required ProgressionDateRange range, required String exerciseKey, }); Future readExerciseSeries({ required ProgressionDateRange range, required String exerciseKey, required ProgressionMeasure measure, }); } final class ProgressionOverview { const ProgressionOverview({ required this.period, required this.rangeLabelKind, required this.completedSessionCount, required this.totalActiveMs, required this.activeWeekCount, required this.totalWeekCount, required this.hasAnyCompletedHistory, required this.exerciseOptions, }); final ProgressionPeriod period; final ProgressionRangeLabelKind rangeLabelKind; final int completedSessionCount; final int totalActiveMs; final int activeWeekCount; final int? totalWeekCount; final bool hasAnyCompletedHistory; final List exerciseOptions; } final class ProgressionExerciseOption { const ProgressionExerciseOption({ required this.exerciseKey, required this.nameSnapshot, required this.isArchived, required this.lastPerformedAt, required this.measures, }); final String exerciseKey; final String nameSnapshot; final bool isArchived; final DateTime lastPerformedAt; final List measures; } final class ProgressionMeasureOption { const ProgressionMeasureOption({ required this.measure, required this.label, this.scoreLabel, this.scoreUnit, required this.lowerIsBetter, }); final ProgressionMeasure measure; final String label; final String? scoreLabel; final String? scoreUnit; final bool lowerIsBetter; } final class ProgressionExerciseSeries { const ProgressionExerciseSeries({ required this.exerciseKey, required this.exerciseNameSnapshot, required this.measure, required this.points, required this.summary, required this.state, }); final String exerciseKey; final String exerciseNameSnapshot; final ProgressionMeasureOption measure; final List points; final ProgressionSeriesSummary summary; final ProgressionSeriesState state; } final class ProgressionPoint { const ProgressionPoint({ required this.workoutHistoryId, required this.workoutNameSnapshot, required this.startedAt, required this.value, required this.rawValue, }); final String workoutHistoryId; final String workoutNameSnapshot; final DateTime startedAt; final double value; final Object rawValue; } final class ProgressionSeriesSummary { const ProgressionSeriesSummary({ required this.kind, this.best, this.last, this.recentTotal, this.lastSessionTotal, }); final ProgressionSeriesSummaryKind kind; final Object? best; final Object? last; final Object? recentTotal; final Object? lastSessionTotal; } abstract interface class StarterSeedStateRepository { Future readAppliedStarterSeedVersion(); Future writeAppliedStarterSeedVersion(int version, DateTime appliedAt); } abstract interface class StarterContentRepository { Future isLocalContentEmpty(); Future insertStarterContent(StarterContent content); } enum RemoteAuthFailure { invalidCredentials, emailAlreadyUsed, network, server, unknown, } final class RemoteAuthException implements Exception { const RemoteAuthException(this.failure, [this.message]); final RemoteAuthFailure failure; final String? message; @override String toString() => message ?? failure.name; } final class RemoteAuthResult { const RemoteAuthResult({ required this.userId, required this.email, required this.token, required this.expiresAt, }); final String userId; final String email; final String token; final DateTime expiresAt; } abstract interface class AuthTokenStore { Future saveToken(String token, DateTime expiresAt); Future readToken(); Future clearToken(); } abstract interface class OnlineAccountRepository { Future currentSession(); Future saveSession(UserAccountSession session); Future clearSession(); } abstract interface class RemoteAuthApi { Future register({ required String email, required String password, String? displayName, }); Future login({ required String email, required String password, }); Future logout(String token); } enum SyncResourceType { exercise, program, workoutTemplate, workoutHistory, mediaAsset, } enum RemoteSyncPushStatus { accepted, ignoredOlder, error } enum OnlineSyncStatus { idle, syncing, success, failure } final class RemoteSyncPushItem { const RemoteSyncPushItem({ required this.resourceType, required this.clientId, required this.schemaVersion, required this.clientUpdatedAt, required this.deletedAt, required this.payload, }); final SyncResourceType resourceType; final String clientId; final int schemaVersion; final DateTime clientUpdatedAt; final DateTime? deletedAt; final Map payload; } final class RemoteSyncPushItemResult { const RemoteSyncPushItemResult({ required this.resourceType, required this.clientId, this.serverId, required this.status, this.serverUpdatedAt, this.errorMessage, }); final SyncResourceType resourceType; final String clientId; final String? serverId; final RemoteSyncPushStatus status; final DateTime? serverUpdatedAt; final String? errorMessage; } final class RemoteSyncPushResult { const RemoteSyncPushResult({ required this.serverCursor, required this.results, }); final String? serverCursor; final List results; } final class RemoteSyncedItem { const RemoteSyncedItem({ required this.resourceType, required this.clientId, required this.serverId, required this.schemaVersion, required this.clientUpdatedAt, required this.serverUpdatedAt, required this.deletedAt, required this.payload, }); final SyncResourceType resourceType; final String clientId; final String serverId; final int schemaVersion; final DateTime clientUpdatedAt; final DateTime serverUpdatedAt; final DateTime? deletedAt; final Map payload; } final class RemoteSyncPullResult { const RemoteSyncPullResult({required this.serverCursor, required this.items}); final String? serverCursor; final List items; } abstract interface class RemoteSyncApi { Future push({ required String deviceId, required List items, required String token, }); Future pull({ required String? since, required String token, }); } final class SyncMetadataSnapshot { const SyncMetadataSnapshot({ this.serverCursor, this.lastSuccessfulSyncAt, this.lastAttemptAt, this.lastFailureAt, this.status = OnlineSyncStatus.idle, this.pendingPushCount, }); final String? serverCursor; final DateTime? lastSuccessfulSyncAt; final DateTime? lastAttemptAt; final DateTime? lastFailureAt; final OnlineSyncStatus status; final int? pendingPushCount; SyncMetadataSnapshot copyWith({ Object? serverCursor = _portsUnchanged, Object? lastSuccessfulSyncAt = _portsUnchanged, Object? lastAttemptAt = _portsUnchanged, Object? lastFailureAt = _portsUnchanged, OnlineSyncStatus? status, Object? pendingPushCount = _portsUnchanged, }) { return SyncMetadataSnapshot( serverCursor: serverCursor == _portsUnchanged ? this.serverCursor : serverCursor as String?, lastSuccessfulSyncAt: lastSuccessfulSyncAt == _portsUnchanged ? this.lastSuccessfulSyncAt : lastSuccessfulSyncAt as DateTime?, lastAttemptAt: lastAttemptAt == _portsUnchanged ? this.lastAttemptAt : lastAttemptAt as DateTime?, lastFailureAt: lastFailureAt == _portsUnchanged ? this.lastFailureAt : lastFailureAt as DateTime?, status: status ?? this.status, pendingPushCount: pendingPushCount == _portsUnchanged ? this.pendingPushCount : pendingPushCount as int?, ); } } final class RemoteResourceMapping { const RemoteResourceMapping({ required this.resourceType, required this.clientId, required this.serverId, required this.serverUpdatedAt, }); final SyncResourceType resourceType; final String clientId; final String serverId; final DateTime serverUpdatedAt; } final class PendingSyncChange { const PendingSyncChange({required this.changeLogIds, required this.item}); final List changeLogIds; final RemoteSyncPushItem item; } abstract interface class SyncMetadataRepository { Future read(); Future save(SyncMetadataSnapshot metadata); } abstract interface class RemoteResourceMappingRepository { Future find({ required SyncResourceType resourceType, required String clientId, }); Future save(RemoteResourceMapping mapping); } abstract interface class LocalSyncChangeRepository { Future> listPendingChanges(); Future markChangesSynced(List changeLogIds, DateTime syncedAt); Future applyRemoteItem(RemoteSyncedItem item); } final class RemoteShareSendResult { const RemoteShareSendResult({ required this.shareId, required this.recipientUserIds, required this.unresolvedEmails, }); final String shareId; final List recipientUserIds; final List unresolvedEmails; } enum ShareSendStatus { sent, notConnected, queued } final class ShareSendResult { const ShareSendResult({ required this.status, this.shareId, this.recipientUserIds = const [], this.unresolvedEmails = const [], this.pendingActionId, }); final ShareSendStatus status; final String? shareId; final List recipientUserIds; final List unresolvedEmails; final String? pendingActionId; } abstract interface class RemoteShareApi { Future sendShare({ required ShareResourceType resourceType, required Map payload, required List recipientEmails, required String token, }); Future> fetchInbox(String token); Future> acceptShare(String shareId, String token); Future declineShare(String shareId, String token); Future revokeShare(String shareId, String token); } abstract interface class ShareInboxRepository { Future> listAll(); Future findByShareId(String shareId); Future upsert(ShareInboxItem item); Future upsertAll(List items); Future markStatus( String shareId, ShareInboxStatus status, DateTime respondedAt, ); } abstract interface class PendingShareActionRepository { Future> listPending(); Future add(PendingShareAction action); Future markSucceeded(String id, DateTime attemptedAt); Future markFailed(String id, DateTime attemptedAt); } const Object _portsUnchanged = Object(); abstract interface class ExerciseRepository { Future findById(String id); Future> listActive(); Future isReferencedByProgram(String id); Future save(Exercise exercise); } final class StoredMediaFile { const StoredMediaFile({ required this.localUri, required this.mimeType, required this.sizeBytes, this.width, this.height, this.durationMs, }); final String localUri; final String? mimeType; final int sizeBytes; final int? width; final int? height; final int? durationMs; } abstract interface class LocalMediaStorage { Future importFile({ required String sourcePath, required MediaKind kind, required String stableFileName, }); Future> listManagedLocalUris(); Future deleteByLocalUri(String localUri); } abstract interface class MediaAssetRepository { Future findById(String id); Future> listActive(); Future save(MediaAsset mediaAsset); } abstract interface class ProgramRepository { Future findById(String id); Future> listActive(); Future save(Program program); Future saveExercise(ProgramExercise exercise); Future replaceExercises(Program program, DateTime deletedAt); } abstract interface class WorkoutTemplateRepository { Future findById(String id); Future> listActive(); Future save(WorkoutTemplate template); Future saveAll(List templates); Future saveProgram(WorkoutTemplateProgram program); Future saveOverride(WorkoutTemplateExerciseOverride override); Future replaceComposition(WorkoutTemplate template, DateTime deletedAt); } abstract interface class ActiveSessionRepository { Future findById(String id); Future findOpen(); Future save(ActiveWorkoutSession session); Future saveSetResult(ActiveSetResult result); Future saveSetTimerState(ActiveSetTimerState state); Future saveRestState(ActiveRestState restState); Future saveScoreStopwatchState(ActiveScoreStopwatchState state); Future saveManualScoreState(ActiveManualScoreState state); Future saveExerciseStepProgressState( ActiveExerciseStepProgressState state, ); Future saveExerciseStepResult(ActiveExerciseStepResult result); Future deleteScoreStopwatchState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, required DateTime deletedAt, }); Future deleteManualScoreState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, required DateTime deletedAt, }); Future findRestStateById(String id); Future findScoreStopwatchState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }); Future findManualScoreState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }); Future findSetTimerState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }); Future findExerciseStepProgressState({ required String sessionId, required int programIndex, required int exerciseIndex, required int setIndex, }); Future> listSetResults(String sessionId); Future> listSetTimerStates(String sessionId); Future> listExerciseStepProgressStates( String sessionId, ); Future> listExerciseStepResults( String sessionId, ); Future> listRestStates(String sessionId); Future> listScoreStopwatchStates( String sessionId, ); Future> listManualScoreStates(String sessionId); } abstract interface class WorkoutHistoryRepository { Future findById(String id); Future> listActive(); Future save(WorkoutHistory history); Future patchHeartRateSummary({ required String historyId, int? minHeartRateBpm, required double averageHeartRateBpm, required int maxHeartRateBpm, double? totalDistanceMeters, double? totalCaloriesKcal, required DateTime patchedAt, }); Future saveSetResult(WorkoutHistorySetResult result); Future saveStepResult(WorkoutHistoryStepResult result); Future delete(String id, DateTime deletedAt); } abstract interface class WorkoutTelemetryRepository { Future saveSample(WorkoutTelemetrySample sample); Future findWindowState(String sessionId); Future saveWindowState(ActiveWorkoutTelemetryWindowState state); Future deleteWindowState(String sessionId); Future> listSamples(String sessionId); Future> listSamplesForScope({ required String sessionId, required WorkoutTelemetryAggregateScope scope, int? programIndex, int? exerciseIndex, int? setIndex, int? passageIndex, int? stepIndex, }); Future replaceAggregatesForSession({ required String sessionId, required List aggregates, }); Future> listAggregates(String sessionId); Future findAggregate({ required String sessionId, required WorkoutTelemetryAggregateScope scope, int? programIndex, int? exerciseIndex, int? setIndex, int? passageIndex, int? stepIndex, }); } final class ActivePerformanceMeasures { const ActivePerformanceMeasures({ required this.timeEnabled, required this.repsEnabled, required this.scoreEnabled, this.scoreInputMode = ScoreInputMode.manual, }); final bool timeEnabled; final bool repsEnabled; final bool scoreEnabled; final ScoreInputMode scoreInputMode; bool get hasAny => timeEnabled || repsEnabled || scoreEnabled; } enum PerformanceMetric { score, reps, time } final class WorkoutHistorySetPerformance { const WorkoutHistorySetPerformance({ required this.workoutHistoryId, required this.startedAt, required this.setIndex, required this.exerciseNameSnapshot, required this.scoreInputModeSnapshot, this.actualTimeMs, this.actualReps, this.actualScore, this.actualScoreTimeMs, this.completedAt, }); final String workoutHistoryId; final DateTime startedAt; final int setIndex; final String exerciseNameSnapshot; final ScoreInputMode scoreInputModeSnapshot; final int? actualTimeMs; final int? actualReps; final double? actualScore; final int? actualScoreTimeMs; final DateTime? completedAt; } final class WorkoutHistoryMetricPerformance { const WorkoutHistoryMetricPerformance({ required this.workoutHistoryId, required this.startedAt, required this.setIndex, required this.exerciseNameSnapshot, required this.metric, required this.scoreInputModeSnapshot, this.actualTimeMs, this.actualReps, this.actualScore, this.actualScoreTimeMs, this.completedAt, }); final String workoutHistoryId; final DateTime startedAt; final int setIndex; final String exerciseNameSnapshot; final PerformanceMetric metric; final ScoreInputMode scoreInputModeSnapshot; final int? actualTimeMs; final int? actualReps; final double? actualScore; final int? actualScoreTimeMs; final DateTime? completedAt; } final class ExercisePerformanceReference { const ExercisePerformanceReference({ required this.hasAnyHistoryForExercise, this.last, this.record, }); final bool hasAnyHistoryForExercise; final WorkoutHistorySetPerformance? last; final WorkoutHistoryMetricPerformance? record; } abstract interface class ExercisePerformanceReferenceRepository { Future hasAnyCompletedHistoryForExercise(String exerciseId); Future findLatestSetPerformance({ required String exerciseId, required ActivePerformanceMeasures activeMeasures, required int currentSetIndex, }); Future findBestMetricPerformance({ required String exerciseId, required PerformanceMetric metric, required ScoreInputMode scoreInputMode, }); } final class SyncRunSummary { const SyncRunSummary({ required this.pushedChanges, required this.pulledChanges, this.skipped = false, this.failed = false, this.message, }); final int pushedChanges; final int pulledChanges; final bool skipped; final bool failed; final String? message; } abstract interface class SyncGateway { Future synchronize(); } final class NoOpSyncGateway implements SyncGateway { const NoOpSyncGateway(); @override Future synchronize() async { return const SyncRunSummary(pushedChanges: 0, pulledChanges: 0); } }